From efe1004c900e21ef5b268f400abe4e47e48e0900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20M=C3=A1r=20Atlason?= <54210288+saevarma@users.noreply.github.com> Date: Fri, 13 Sep 2024 07:54:31 +0000 Subject: [PATCH 001/173] fix(auth-admin-api): Add missing xroad config to auth admin api (#15988) --- apps/services/auth/admin-api/infra/auth-admin-api.ts | 4 ++-- charts/identity-server/values.dev.yaml | 5 +++++ charts/identity-server/values.prod.yaml | 5 +++++ charts/identity-server/values.staging.yaml | 5 +++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index 08d7b9593a46..4d7c3a6dcadf 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -4,7 +4,7 @@ import { service, ServiceBuilder, } from '../../../../../infra/src/dsl/dsl' -import { RskProcuring } from '../../../../../infra/src/dsl/xroad' +import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' const REDIS_NODE_CONFIG = { dev: json([ @@ -67,7 +67,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', }) - .xroad(RskProcuring) + .xroad(Base, Client, RskProcuring) .ingress({ primary: { host: { diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index 521fb140de1c..1bda1fd93566 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -224,12 +224,17 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -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_ACTOR_TOKEN: 'true' 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_RSK_PROCURING_ACTOR_TOKEN: 'true' XROAD_RSK_PROCURING_PATH: 'IS-DEV/GOV/10006/Skatturinn/relationships-v1' XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' + XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is' + XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index c94752919973..438409b0622b 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -221,12 +221,17 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -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_ACTOR_TOKEN: 'true' XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS/GOV/6503760649/SKRA-Protected/Einstaklingar-v1' XROAD_RSK_PROCURING_ACTOR_TOKEN: 'true' XROAD_RSK_PROCURING_PATH: 'IS/GOV/5402696029/Skatturinn/relationships-v1' XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' + XROAD_TLS_BASE_PATH: 'https://securityserver.island.is' + XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index e789cbc9e34c..b5b7ffb0f920 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -224,12 +224,17 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -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_ACTOR_TOKEN: 'true' 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_RSK_PROCURING_ACTOR_TOKEN: 'true' XROAD_RSK_PROCURING_PATH: 'IS-TEST/GOV/5402696029/Skatturinn/relationships-v1' XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' + XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is' + XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' From a4abd7b381c5aa8c691de55ed26289d6b3e114d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3n=20Levy?= Date: Fri, 13 Sep 2024 08:03:07 +0000 Subject: [PATCH 002/173] ci: fix upload-artifact breaking changes (#15989) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .github/workflows/pullrequest.yml | 4 +++- .github/workflows/push.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index f6fb18896c43..aef35c5de860 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -73,11 +73,13 @@ jobs: # See this for more information: # https://github.blog/changelog/2020-10-08-github-actions-ability-to-change-retention-days-for-artifacts-and-logs/ - name: Keep PR run event - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b18b1d32f3f31abcdc29dee3f2484801fe7822f4 with: name: pr-event path: event.json retention-days: 90 + include-hidden-files: true + if-no-files-found: error - name: Get cache id: get-cache diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e9f872ed91f8..490f7cb1abb9 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -238,12 +238,14 @@ jobs: # See this for more information: # https://github.blog/changelog/2020-10-08-github-actions-ability-to-change-retention-days-for-artifacts-and-logs/ - name: Keep PR run event - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b18b1d32f3f31abcdc29dee3f2484801fe7822f4 if: needs.pre-checks.outputs.PRE_CHECK && needs.pre-checks.outputs.PRE_CHECK == 'feature-deploy' with: name: pr-event path: event.json retention-days: 90 + include-hidden-files: true + if-no-files-found: error - name: Generate nodejs image tag id: nodejs_image From f9c2668abab8a2c28366d63c008ef819cae7b1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:13:51 +0000 Subject: [PATCH 003/173] feat(web): Parental leave calculator - Calculation formula updated (#15981) * WIP * Update formula * Remove unused code --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../ParentalLeaveCalculator.tsx | 229 ++++-------------- .../ParentalLeaveCalculator/utils.ts | 199 +++++++++++++++ 2 files changed, 248 insertions(+), 180 deletions(-) create mode 100644 apps/web/components/connected/ParentalLeaveCalculator/utils.ts diff --git a/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx index f2aaeed20a3a..6d0fca31a252 100644 --- a/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx +++ b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx @@ -31,6 +31,7 @@ import { formatCurrency as formatCurrencyUtil } from '@island.is/web/utils/curre import { MarkdownText } from '../../Organization' import { translations as t } from './translations.strings' +import { Calculator, Status, Union, WorkPercentage } from './utils' import * as styles from './ParentalLeaveCalculator.css' interface FieldProps { @@ -64,17 +65,6 @@ const Field = ({ ) } -enum Status { - PARENTAL_LEAVE = 'parentalLeave', - STUDENT = 'student', - OUTSIDE_WORKFORCE = 'outsideWorkForce', -} - -enum WorkPercentage { - OPTION_1 = 'option1', - OPTION_2 = 'option2', -} - enum ParentalLeavePeriod { MONTH = 'month', THREE_WEEKS = 'threeWeeks', @@ -146,8 +136,7 @@ const FormScreen = ({ slice, changeScreen }: ScreenProps) => { }, [formatMessage, slice.configJson?.additionalPensionFundingOptions]) const unionOptions = useMemo[]>(() => { - const options: { label: string; percentage: number }[] = slice.configJson - ?.unionOptions + const options: Union[] = slice.configJson?.unionOptions ? [...slice.configJson.unionOptions] : [] @@ -532,6 +521,10 @@ const calculateResults = ( }, slice: ParentalLeaveCalculatorProps['slice'], ) => { + if (typeof input.birthyear !== 'number') { + return null + } + const yearConfig = slice.configJson?.yearConfig?.[String(input.birthyear)] const parseResult = yearConfigSchema.safeParse(yearConfig) if (!parseResult.success) { @@ -554,179 +547,55 @@ const calculateResults = ( pensionFundingRequiredPercentage: parseResult.data['Skyldu lífeyrir'], } - let mainResultBeforeDeduction = 0 - let mainResultAfterDeduction = 0 - let unionFee = 0 - let totalTax = 0 - let usedPersonalDiscount = 0 - let additionalPensionFunding = 0 - let pensionFunding = 0 - - if (input.status === Status.STUDENT) { - mainResultBeforeDeduction = constants.parentalLeaveStudent - - let taxStep = 1 - - if (mainResultBeforeDeduction > constants.taxBracket1) { - taxStep = 2 - } else if (mainResultBeforeDeduction > constants.taxBracket2) { - taxStep = 3 - } - - if (taxStep === 1) { - totalTax = mainResultBeforeDeduction * (constants.taxRate1 / 100) - } else if (taxStep === 2) { - totalTax = mainResultBeforeDeduction * (constants.taxRate2 / 100) - } else if (taxStep === 3) { - totalTax = mainResultBeforeDeduction * (constants.taxRate3 / 100) - } - - usedPersonalDiscount = - constants.personalDiscount * ((input.personalDiscount ?? 100) / 100) - - if (usedPersonalDiscount > totalTax) { - usedPersonalDiscount = totalTax - } - - mainResultAfterDeduction = mainResultBeforeDeduction - - mainResultAfterDeduction = - mainResultAfterDeduction - (totalTax - usedPersonalDiscount) - } - - if (input.status === Status.OUTSIDE_WORKFORCE) { - mainResultBeforeDeduction = constants.parentalLeaveGeneral - - let taxStep = 1 - - if (mainResultBeforeDeduction > constants.taxBracket1) { - taxStep = 2 - } else if (mainResultBeforeDeduction > constants.taxBracket2) { - taxStep = 3 - } - - if (taxStep === 1) { - totalTax = mainResultBeforeDeduction * (constants.taxRate1 / 100) - } else if (taxStep === 2) { - totalTax = mainResultBeforeDeduction * (constants.taxRate2 / 100) - } else if (taxStep === 3) { - totalTax = mainResultBeforeDeduction * (constants.taxRate3 / 100) - } - - usedPersonalDiscount = - constants.personalDiscount * ((input.personalDiscount ?? 100) / 100) - - if (usedPersonalDiscount > totalTax) { - usedPersonalDiscount = totalTax - } - - mainResultAfterDeduction = mainResultBeforeDeduction - - mainResultAfterDeduction = - mainResultAfterDeduction - (totalTax - usedPersonalDiscount) - } - - if (input.status === Status.PARENTAL_LEAVE) { - if ( - typeof input.income !== 'number' || - typeof input.parentalLeaveRatio !== 'number' - ) { - return null - } - - mainResultBeforeDeduction = - input.income * (constants.parentalLeaveRatio / 100) - - if ( - input.workPercentage === WorkPercentage.OPTION_1 && - mainResultBeforeDeduction < constants.parentalLeaveLow - ) { - mainResultBeforeDeduction = constants.parentalLeaveLow - } else if ( - input.workPercentage === WorkPercentage.OPTION_2 && - mainResultBeforeDeduction < constants.parentalLeaveHigh - ) { - mainResultBeforeDeduction = constants.parentalLeaveHigh - } - - if (mainResultBeforeDeduction > constants.maxIncome) { - mainResultBeforeDeduction = constants.maxIncome - } - - mainResultBeforeDeduction *= input.parentalLeaveRatio / 100 + const unionOptions: Union[] = slice.configJson?.unionOptions ?? [] - let paternityLeavePeriodMultiplier = 1 - - if (input.parentalLeavePeriod === ParentalLeavePeriod.THREE_WEEKS) { - paternityLeavePeriodMultiplier = 3 / 4 - } else if (input.parentalLeavePeriod === ParentalLeavePeriod.TWO_WEEKS) { - paternityLeavePeriodMultiplier = 1 / 2 - } - - mainResultBeforeDeduction *= paternityLeavePeriodMultiplier - - let taxStep = 1 - - if (mainResultBeforeDeduction > constants.taxBracket1) { - taxStep = 2 - } else if (mainResultBeforeDeduction > constants.taxBracket2) { - taxStep = 3 - } - - if (taxStep === 1) { - totalTax = mainResultBeforeDeduction * (constants.taxRate1 / 100) - } else if (taxStep === 2) { - totalTax = mainResultBeforeDeduction * (constants.taxRate2 / 100) - } else if (taxStep === 3) { - totalTax = mainResultBeforeDeduction * (constants.taxRate3 / 100) - } - - usedPersonalDiscount = - constants.personalDiscount * - ((input.personalDiscount ?? 0) / 100) * - paternityLeavePeriodMultiplier - - if (usedPersonalDiscount > totalTax) { - usedPersonalDiscount = totalTax - } - - additionalPensionFunding = - mainResultBeforeDeduction * - ((input.additionalPensionFundingPercentage ?? 0) / 100) - - pensionFunding = - mainResultBeforeDeduction * - (constants.pensionFundingRequiredPercentage / 100) - - const unionOptions: { label: string; percentage: number }[] = - slice.configJson?.unionOptions ?? [] - - unionFee = - mainResultBeforeDeduction * - ((unionOptions.find((option) => option.label === input.union) - ?.percentage ?? 0) / - 100) + const calculator = new Calculator( + { + percentOfEarningsPaid: constants.parentalLeaveRatio, + tax1: constants.taxRate1, + tax2: constants.taxRate2, + tax3: constants.taxRate3, + tax1Amount: constants.taxBracket1, + tax2Amount: constants.taxBracket2, + taxDiscount: constants.personalDiscount, + pGrantStudents: constants.parentalLeaveStudent, + pGrant: constants.parentalLeaveGeneral, + pGrantHigher: constants.parentalLeaveHigh, + pGrantLower: constants.parentalLeaveLow, + maxEarnings: constants.maxIncome, + mandatoryPensionPercentage: constants.pensionFundingRequiredPercentage, + }, + { + status: input.status ?? Status.PARENTAL_LEAVE, + childYearBorn: input.birthyear, + workPercentage: input.workPercentage ?? WorkPercentage.OPTION_1, + averageEarningsPerMonth: input.income ?? 0, + monthsInPL: + input.parentalLeavePeriod === ParentalLeavePeriod.TWO_WEEKS + ? 1 / 2 + : input.parentalLeavePeriod === ParentalLeavePeriod.THREE_WEEKS + ? 3 / 4 + : 1, + union: unionOptions.find((option) => option.label === input.union), + taxDiscountRate: input.personalDiscount ?? 100, + extraPension: input.additionalPensionFundingPercentage ?? 0, + plRate: input.parentalLeaveRatio ?? 100, + }, + ) - /* --- After deduction --- */ - mainResultAfterDeduction = mainResultBeforeDeduction - mainResultAfterDeduction -= unionFee - mainResultAfterDeduction = - mainResultAfterDeduction - (totalTax - usedPersonalDiscount) - mainResultAfterDeduction -= additionalPensionFunding - mainResultAfterDeduction -= pensionFunding - } + const results = calculator.calculateResults() return { + constants, results: { - mainResultBeforeDeduction, - mainResultAfterDeduction, - unionFee, - pensionFunding, - totalTax, - usedPersonalDiscount, - additionalPensionFunding, + mainResultBeforeDeduction: results.plBruttoPerMonth, + mainResultAfterDeduction: results.plNettoPerMonth, + unionFee: results.unionFees, + pensionFunding: results.pensionPerMonth, + totalTax: results.tax, + usedPersonalDiscount: results.taxDiscount, + additionalPensionFunding: results.extraPensionPerMonth, }, - constants, } } diff --git a/apps/web/components/connected/ParentalLeaveCalculator/utils.ts b/apps/web/components/connected/ParentalLeaveCalculator/utils.ts new file mode 100644 index 000000000000..fecf2858e9b2 --- /dev/null +++ b/apps/web/components/connected/ParentalLeaveCalculator/utils.ts @@ -0,0 +1,199 @@ +export enum Status { + PARENTAL_LEAVE = 'parentalLeave', + STUDENT = 'student', + OUTSIDE_WORKFORCE = 'outsideWorkForce', +} + +export enum WorkPercentage { + OPTION_1 = 'option1', + OPTION_2 = 'option2', +} + +export type Union = { + label: string + percentage: number + fixedCost: number +} + +type Constants = { + /* Hlutfall fæðingarorlofs (fasti) */ + percentOfEarningsPaid: number + + /* Persónuafsláttur */ + taxDiscount: number + + /* Skattprósentur */ + tax1: number + tax2: number + tax3: number + + /* Skattmörk */ + tax1Amount: number + tax2Amount: number + + /* Fæðingarstyrkur námsmanna */ + pGrantStudents: number + + /* Fæðingarstyrkur almennur */ + pGrant: number + + /* Fæðingarstyrkur hærri */ + pGrantHigher: number + + /* Fæðingarstyrkur lægri */ + pGrantLower: number + + /* Hámarks laun fyrir fæðingarorlof */ + maxEarnings: number + + /* Skyldu lífeyrir */ + mandatoryPensionPercentage: number +} + +type Input = { + status: Status + + /* Fæðingarár barns */ + childYearBorn: number + + /* Starfshlutfall á innlendum vinnumarkaði í 6 mánuði fyrir áætlaðan fæðingardag */ + workPercentage: WorkPercentage + + /* Meðaltekjur innanlands á mánuði */ + averageEarningsPerMonth: number + + /* Fjöldi mánaða í fæðingarorlofi */ + monthsInPL: number + + /* Stéttarfélag */ + union: Union | undefined + + /* Persónuafsláttur */ + taxDiscountRate: number + + /* Viðbótar lifeyrissjóður */ + extraPension: number + + /* Hlutfall fæðingarorlofs */ + plRate: number +} + +export class Calculator { + private constants: Constants + private input: Input + + constructor(constants: Constants, input: Input) { + this.constants = constants + this.input = input + } + + private calculateTax(amount: number) { + const intAmount = amount + let taxAmount = 0.0 + + // Fyrsta skattþrep + if (intAmount <= this.constants.tax1Amount) { + taxAmount = intAmount * (this.constants.tax1 / 100) + } + // Annað skattþrep + else if (intAmount <= this.constants.tax2Amount) { + taxAmount = this.constants.tax1Amount * (this.constants.tax1 / 100) + taxAmount += + (intAmount - this.constants.tax1Amount) * (this.constants.tax2 / 100) + } + // Þriðja skattþrep + else { + taxAmount = this.constants.tax1Amount * (this.constants.tax1 / 100) + taxAmount += + (this.constants.tax2Amount - this.constants.tax1Amount) * + (this.constants.tax2 / 100) + taxAmount += + (intAmount - this.constants.tax2Amount) * (this.constants.tax3 / 100) + } + + return Math.round(taxAmount) + } + + public calculateResults() { + const myAverage = + (this.input.averageEarningsPerMonth * + this.constants.percentOfEarningsPaid) / + 100 + + const earningsPerMonth = Math.min(myAverage, this.constants.maxEarnings) + let plBruttoPerMonth = earningsPerMonth + + let fullAmount = 0 + let pensionPerMonth = 0 + let extraPensionPerMonth = 0 + let unionFees = 0 + + if (this.input.status === Status.STUDENT) { + plBruttoPerMonth = this.constants.pGrantStudents + fullAmount = plBruttoPerMonth + } + + if (this.input.status === Status.OUTSIDE_WORKFORCE) { + plBruttoPerMonth = this.constants.pGrant + fullAmount = plBruttoPerMonth + } + + if (this.input.status === Status.PARENTAL_LEAVE) { + plBruttoPerMonth = Math.max( + this.input.workPercentage === WorkPercentage.OPTION_1 + ? this.constants.pGrantLower + : this.constants.pGrantHigher, + earningsPerMonth, + ) + + fullAmount = plBruttoPerMonth + + plBruttoPerMonth = (plBruttoPerMonth * this.input.plRate) / 100 + + if (this.input.monthsInPL < 1) { + plBruttoPerMonth *= this.input.monthsInPL + } + + pensionPerMonth = + (plBruttoPerMonth * this.constants.mandatoryPensionPercentage) / 100 + extraPensionPerMonth = (plBruttoPerMonth * this.input.extraPension) / 100 + + if (this.input.union) { + unionFees = + this.input.union.fixedCost > 0 + ? Math.round(this.input.union.fixedCost) + : Math.round(plBruttoPerMonth * (this.input.union.percentage / 100)) + } + } + + const tax = this.calculateTax( + plBruttoPerMonth - pensionPerMonth - extraPensionPerMonth, + ) + + let taxDiscount = + this.constants.taxDiscount * (this.input.taxDiscountRate / 100) + + if (tax <= taxDiscount) { + taxDiscount = tax + } + + const plNettoPerMonth = + plBruttoPerMonth - + pensionPerMonth - + extraPensionPerMonth - + unionFees - + tax + + taxDiscount + + return { + plBruttoPerMonth, + plNettoPerMonth, + unionFees, + pensionPerMonth, + extraPensionPerMonth, + tax, + taxDiscount, + fullAmount, + } + } +} From 554376348d07814dec74f14855629b7a1d1c02b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:31:32 +0000 Subject: [PATCH 004/173] feat(web): Generic List - UX improvements (#15983) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../components/GenericList/GenericList.css.ts | 4 + .../components/GenericList/GenericList.tsx | 83 ++++++++++--------- .../Slice/TeamListSlice/TeamListSlice.tsx | 4 +- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/apps/web/components/GenericList/GenericList.css.ts b/apps/web/components/GenericList/GenericList.css.ts index 9757125080eb..3aaa6a2380ef 100644 --- a/apps/web/components/GenericList/GenericList.css.ts +++ b/apps/web/components/GenericList/GenericList.css.ts @@ -3,3 +3,7 @@ import { style } from '@vanilla-extract/css' export const clickableItemTopRowContainer = style({ minHeight: '30px', }) + +export const filterTagsContainer = style({ + minHeight: '32px', +}) diff --git a/apps/web/components/GenericList/GenericList.tsx b/apps/web/components/GenericList/GenericList.tsx index a25fc35baf2b..cc119bab90e6 100644 --- a/apps/web/components/GenericList/GenericList.tsx +++ b/apps/web/components/GenericList/GenericList.tsx @@ -332,6 +332,7 @@ export const GenericList = ({ variant={isMobile ? 'dialog' : 'popover'} onFilterClear={() => { setParameters(null) + setPage(null) }} filterInput={filterInputComponent} > @@ -342,6 +343,7 @@ export const GenericList = ({ : 'Clear selection' } onChange={({ categoryId, selected }) => { + setPage(null) setParameters((prevParameters) => { // Make sure we clear out the query params from the url when there is nothing selected if ( @@ -361,6 +363,7 @@ export const GenericList = ({ }) }} onClear={(categoryId) => { + setPage(null) setParameters((prevParameters) => { const updatedParameters = { ...prevParameters, @@ -384,43 +387,45 @@ export const GenericList = ({ - - {selectedFilters.length > 0 && ( - - {activeLocale === 'is' ? 'Síað eftir:' : 'Filtered by:'} - - )} - - {selectedFilters.map(({ value, label, category }) => ( - { - setParameters((prevParameters) => { - const updatedParameters = { - ...prevParameters, - [category]: ( - prevParameters?.[category] ?? [] - ).filter((prevValue) => prevValue !== value), - } - - // Make sure we clear out the query params from the url when there is nothing selected - if ( - Object.values(updatedParameters).every( - (s) => !s || s.length === 0, - ) - ) { - return null - } - - return updatedParameters - }) - }} - > - {label} - - ))} + + + {selectedFilters.length > 0 && ( + + {activeLocale === 'is' ? 'Síað eftir:' : 'Filtered by:'} + + )} + + {selectedFilters.map(({ value, label, category }) => ( + { + setParameters((prevParameters) => { + const updatedParameters = { + ...prevParameters, + [category]: ( + prevParameters?.[category] ?? [] + ).filter((prevValue) => prevValue !== value), + } + + // Make sure we clear out the query params from the url when there is nothing selected + if ( + Object.values(updatedParameters).every( + (s) => !s || s.length === 0, + ) + ) { + return null + } + + return updatedParameters + }) + }} + > + {label} + + ))} + - + )} {filterCategories.length === 0 && filterInputComponent} @@ -436,7 +441,7 @@ export const GenericList = ({ } /> )} - {totalItems === 0 && !displayError && ( + {totalItems === 0 && !displayError && !loading && ( {noResultsFoundText} )} {totalItems > 0 && ( @@ -511,7 +516,7 @@ export const GenericListWrapper = ({ useState(null) const [errorOccurred, setErrorOccurred] = useState(false) - const [fetchListItems, { loading }] = useLazyQuery< + const [fetchListItems, { loading, called }] = useLazyQuery< Query, GetGenericListItemsQueryVariables >(GET_GENERIC_LIST_ITEMS_QUERY, { @@ -567,7 +572,7 @@ export const GenericListWrapper = ({ }) }} totalItems={totalItems} - loading={loading} + loading={loading || !called} pageQueryId={pageQueryId} searchQueryId={searchQueryId} tagQueryId={tagQueryId} diff --git a/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx b/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx index 439a3602dcc7..49ac0aca1107 100644 --- a/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx +++ b/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx @@ -34,7 +34,7 @@ export const TeamMemberListWrapper = ({ ) const [errorOccurred, setErrorOccurred] = useState(false) - const [fetchListItems, { loading }] = useLazyQuery( + const [fetchListItems, { loading, called }] = useLazyQuery( GET_TEAM_MEMBERS_QUERY, { onCompleted(data) { @@ -89,7 +89,7 @@ export const TeamMemberListWrapper = ({ }) }} totalItems={totalItems} - loading={loading} + loading={loading || !called} pageQueryId={pageQueryId} searchQueryId={searchQueryId} tagQueryId={tagQueryId} From 83f57251ef6becace5f04be162356cd64b077e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20M=C3=A1r=20Atlason?= <54210288+saevarma@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:47:10 +0000 Subject: [PATCH 005/173] fix(auth-admin-api): Add missing env (#15991) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/services/auth/admin-api/infra/auth-admin-api.ts | 1 + charts/identity-server/values.dev.yaml | 1 + charts/identity-server/values.prod.yaml | 1 + charts/identity-server/values.staging.yaml | 1 + 4 files changed, 4 insertions(+) diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index 4d7c3a6dcadf..fe1fe79e2480 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -26,6 +26,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { name: 'servicesauth', }) .env({ + IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/auth-api', IDENTITY_SERVER_ISSUER_URL: { dev: 'https://identity-server.dev01.devland.is', staging: 'https://identity-server.staging01.devland.is', diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index 1bda1fd93566..6795fd24a227 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -219,6 +219,7 @@ services-auth-admin-api: DB_NAME: 'servicesauth' DB_REPLICAS_HOST: 'postgres-applications-reader.internal' DB_USER: 'servicesauth' + IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/auth-api' IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.dev01.devland.is' IDENTITY_SERVER_ISSUER_URL_LIST: '["https://identity-server.dev01.devland.is","https://identity-server.staging01.devland.is","https://innskra.island.is"]' LOG_LEVEL: 'info' diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 438409b0622b..96bafb5febeb 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -216,6 +216,7 @@ services-auth-admin-api: DB_NAME: 'servicesauth' DB_REPLICAS_HOST: 'postgres-ids.internal' DB_USER: 'servicesauth' + IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/auth-api' IDENTITY_SERVER_ISSUER_URL: 'https://innskra.island.is' IDENTITY_SERVER_ISSUER_URL_LIST: '["https://innskra.island.is"]' LOG_LEVEL: 'info' diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index b5b7ffb0f920..7c5eeccdd189 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -219,6 +219,7 @@ services-auth-admin-api: DB_NAME: 'servicesauth' DB_REPLICAS_HOST: 'postgres-applications.internal' DB_USER: 'servicesauth' + IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/auth-api' IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.staging01.devland.is' IDENTITY_SERVER_ISSUER_URL_LIST: '["https://identity-server.staging01.devland.is","https://innskra.island.is"]' LOG_LEVEL: 'info' From b484a257732da834667c361af109090745b35994 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:59:59 +0000 Subject: [PATCH 006/173] fix(samrit): digital license (#15979) * fix(samrit): digital license * tweak --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../announcement-of-death/src/lib/messages.ts | 2 +- .../src/forms/application.ts | 2 ++ .../sectionDigitalLicenseInfo.ts | 26 +++++++++++++++++++ .../src/lib/messages.ts | 22 ++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 libs/application/templates/driving-license-duplicate/src/forms/applicationSections/sectionDigitalLicenseInfo.ts diff --git a/libs/application/templates/announcement-of-death/src/lib/messages.ts b/libs/application/templates/announcement-of-death/src/lib/messages.ts index c75ed61bde01..db3556c25385 100644 --- a/libs/application/templates/announcement-of-death/src/lib/messages.ts +++ b/libs/application/templates/announcement-of-death/src/lib/messages.ts @@ -228,7 +228,7 @@ export const m = defineMessages({ description: 'Firearms step answer no', }, firearmsApplicantHeader: { - id: 'aod.application:firearmsApplicantHeader', + id: 'aod.application:firearmsApplicantHeader#markdown', defaultMessage: 'Skráning vörsluaðila:', description: '', }, diff --git a/libs/application/templates/driving-license-duplicate/src/forms/application.ts b/libs/application/templates/driving-license-duplicate/src/forms/application.ts index b00908468b87..ff6f3db73e59 100644 --- a/libs/application/templates/driving-license-duplicate/src/forms/application.ts +++ b/libs/application/templates/driving-license-duplicate/src/forms/application.ts @@ -8,6 +8,7 @@ import { sectionPayment } from './applicationSections/sectionPayment' import { m } from '../lib/messages' import { sectionFakeData } from './applicationSections/sectionFakeData' import { sectionReasonForApplication } from './applicationSections/sectionReasonForApplication' +import { sectionDigitalLicenseInfo } from './applicationSections/sectionDigitalLicenseInfo' export const getApplication = ({ allowFakeData = false }): Form => { return buildForm({ @@ -28,6 +29,7 @@ export const getApplication = ({ allowFakeData = false }): Form => { sectionReasonForApplication, sectionInformation, sectionDelivery, + sectionDigitalLicenseInfo, sectionOverview, sectionPayment, ], diff --git a/libs/application/templates/driving-license-duplicate/src/forms/applicationSections/sectionDigitalLicenseInfo.ts b/libs/application/templates/driving-license-duplicate/src/forms/applicationSections/sectionDigitalLicenseInfo.ts new file mode 100644 index 000000000000..a812eaea4213 --- /dev/null +++ b/libs/application/templates/driving-license-duplicate/src/forms/applicationSections/sectionDigitalLicenseInfo.ts @@ -0,0 +1,26 @@ +import { + buildAlertMessageField, + buildMultiField, + buildSection, +} from '@island.is/application/core' +import { m } from '../../lib/messages' + +export const sectionDigitalLicenseInfo = buildSection({ + id: 'digitalLicenseInfo', + title: m.digitalLicenseInfoTitle, + children: [ + buildMultiField({ + id: 'info', + title: m.digitalLicenseInfoTitle, + description: m.digitalLicenseInfoDescription, + children: [ + buildAlertMessageField({ + id: 'digitalLicenseInfo', + title: m.digitalLicenseInfoAlertTitle, + message: m.digitalLicenseInfoAlertMessage, + alertType: 'info', + }), + ], + }), + ], +}) diff --git a/libs/application/templates/driving-license-duplicate/src/lib/messages.ts b/libs/application/templates/driving-license-duplicate/src/lib/messages.ts index 8361a91e58d1..363c3372b578 100644 --- a/libs/application/templates/driving-license-duplicate/src/lib/messages.ts +++ b/libs/application/templates/driving-license-duplicate/src/lib/messages.ts @@ -257,6 +257,28 @@ export const m = defineMessages({ description: 'Placeholder for office selection', }, + /* Digital License Section */ + digitalLicenseInfoTitle: { + id: 'dld.application:digitalLicenseInfoTitle', + defaultMessage: 'Stafrænt ökuskírteini', + description: 'Digital driving license', + }, + digitalLicenseInfoDescription: { + id: 'dld.application:digitalLicenseInfoDescription', + defaultMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + description: 'Digital driving license', + }, + digitalLicenseInfoAlertTitle: { + id: 'dld.application:digitalLicenseInfoAlertTitle', + defaultMessage: 'Athugið', + description: 'Digital driving license', + }, + digitalLicenseInfoAlertMessage: { + id: 'dld.application:digitalLicenseInfoAlertMessage#markdown', + defaultMessage: 'Þú ert að sækja um samrit ökuskírteinis.', + description: 'Digital driving license', + }, + /* Overview Section */ overviewTitle: { id: 'dld.application:overview.title', From 7651100ebbfd8954eddf886104558cedec0a21ab Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:28:41 +0000 Subject: [PATCH 007/173] fix(efs): Set up for separate feature flag (#15974) * fix(efs): Set up for separate feature flag * message tooltip --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/forms/prerequisites.ts | 9 ++++- .../src/lib/InheritanceReportTemplate.ts | 24 ++++++++++--- .../src/lib/getApplicationFeatureFlags.ts | 34 +++++++++++++++++++ .../inheritance-report/src/lib/messages.ts | 5 +++ libs/feature-flags/src/lib/features.ts | 1 - 5 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 libs/application/templates/inheritance-report/src/lib/getApplicationFeatureFlags.ts diff --git a/libs/application/templates/inheritance-report/src/forms/prerequisites.ts b/libs/application/templates/inheritance-report/src/forms/prerequisites.ts index 52d7119c5e8a..74b17ab2358b 100644 --- a/libs/application/templates/inheritance-report/src/forms/prerequisites.ts +++ b/libs/application/templates/inheritance-report/src/forms/prerequisites.ts @@ -9,7 +9,10 @@ import { DefaultEvents, Form, FormModes } from '@island.is/application/types' import { ESTATE_INHERITANCE, PREPAID_INHERITANCE } from '../lib/constants' import { m } from '../lib/messages' -export const getForm = (): Form => +export const getForm = ({ + allowEstateApplication = false, + allowPrepaidApplication = false, +}): Form => buildForm({ id: 'PrerequisitesDraft', title: '', @@ -34,10 +37,14 @@ export const getForm = (): Form => { value: ESTATE_INHERITANCE, label: m.preDataCollectionApplicationForDefault, + disabled: !allowEstateApplication, + //TODO: remove tooltip when this application is ready to go live + tooltip: m.preDataCollectionApplicationForDefaultTooltip, }, { value: PREPAID_INHERITANCE, label: m.preDataCollectionApplicationForPrepaid, + disabled: !allowPrepaidApplication, }, ], }), diff --git a/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts b/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts index 8ad9f70b01fa..960ee83d0bc1 100644 --- a/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts +++ b/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts @@ -25,8 +25,12 @@ import { Roles, States, } from './constants' -import { Features } from '@island.is/feature-flags' import { EstateOnEntryApi, MaritalStatusApi } from '../dataProviders' +import { FeatureFlagClient } from '@island.is/feature-flags' +import { + getApplicationFeatureFlags, + InheritanceReportFeatureFlags, +} from './getApplicationFeatureFlags' const configuration = ApplicationConfigurations[ApplicationTypes.INHERITANCE_REPORT] @@ -50,7 +54,6 @@ const InheritanceReportTemplate: ApplicationTemplate< institution: m.institution, dataSchema: inheritanceReportSchema, translationNamespaces: [configuration.translation], - featureFlag: Features.inheritanceReport, allowMultipleApplicationsInDraft: false, stateMachineConfig: { initial: States.prerequisites, @@ -64,12 +67,25 @@ const InheritanceReportTemplate: ApplicationTemplate< roles: [ { id: Roles.ESTATE_INHERITANCE_APPLICANT, - formLoader: async () => { + formLoader: async ({ featureFlagClient }) => { + const featureFlags = await getApplicationFeatureFlags( + featureFlagClient as FeatureFlagClient, + ) + const getForm = await import('../forms/prerequisites').then( (val) => val.getForm, ) - return getForm() + return getForm({ + allowEstateApplication: + featureFlags[ + InheritanceReportFeatureFlags.AllowEstateApplication + ], + allowPrepaidApplication: + featureFlags[ + InheritanceReportFeatureFlags.AllowPrepaidApplication + ], + }) }, actions: [{ event: 'SUBMIT', name: '', type: 'primary' }], write: 'all', diff --git a/libs/application/templates/inheritance-report/src/lib/getApplicationFeatureFlags.ts b/libs/application/templates/inheritance-report/src/lib/getApplicationFeatureFlags.ts new file mode 100644 index 000000000000..99024009cd67 --- /dev/null +++ b/libs/application/templates/inheritance-report/src/lib/getApplicationFeatureFlags.ts @@ -0,0 +1,34 @@ +import { FeatureFlagClient } from '@island.is/feature-flags' + +export enum InheritanceReportFeatureFlags { + AllowEstateApplication = 'isInheritanceReportApplicationEnabled', + AllowPrepaidApplication = 'isInheritanceReportPrepaidApplicationEnabled', +} + +export const getApplicationFeatureFlags = async ( + client: FeatureFlagClient, +): Promise> => { + const featureFlags: InheritanceReportFeatureFlags[] = [ + InheritanceReportFeatureFlags.AllowEstateApplication, + InheritanceReportFeatureFlags.AllowPrepaidApplication, + ] + + return ( + await Promise.all( + featureFlags.map(async (key: InheritanceReportFeatureFlags) => { + return { key, value: !!(await client.getValue(key, false)) } + }), + ) + ).reduce( + ( + acc, + { key, value }: { key: InheritanceReportFeatureFlags; value: boolean }, + ) => { + return { + ...acc, + [key]: value, + } + }, + {} as Record, + ) +} diff --git a/libs/application/templates/inheritance-report/src/lib/messages.ts b/libs/application/templates/inheritance-report/src/lib/messages.ts index 6e5341b35271..9c0d900d2423 100644 --- a/libs/application/templates/inheritance-report/src/lib/messages.ts +++ b/libs/application/templates/inheritance-report/src/lib/messages.ts @@ -61,6 +61,11 @@ export const m = defineMessages({ defaultMessage: 'Dánarbú', description: '', }, + preDataCollectionApplicationForDefaultTooltip: { + id: 'ir.application:preDataCollectionApplicationForDefaultTooltip', + defaultMessage: 'Ekki er búið að opna fyrir umsóknir af þessari gerð.', + description: '', + }, // Application begin applicationName: { id: 'ir.application:applicationName', diff --git a/libs/feature-flags/src/lib/features.ts b/libs/feature-flags/src/lib/features.ts index 5abd7e28aad8..eb5bde4c7e7d 100644 --- a/libs/feature-flags/src/lib/features.ts +++ b/libs/feature-flags/src/lib/features.ts @@ -11,7 +11,6 @@ export enum Features { passportApplication = 'isPassportApplicationEnabled', passportAnnulmentApplication = 'isPassportAnnulmentApplicationEnabled', financialStatementInao = 'financialStatementInao', - inheritanceReport = 'isInheritanceReportApplicationEnabled', transportAuthorityDigitalTachographCompanyCard = 'isTransportAuthorityDigitalTachographCompanyCardEnabled', transportAuthorityDigitalTachographWorkshopCard = 'isTransportAuthorityDigitalTachographWorkshopCardEnabled', alcoholTaxRedemption = 'isAlcoholTaxRedemptionEnabled', From c48134d1f4621f25c0c931097a6a0b98671e1823 Mon Sep 17 00:00:00 2001 From: kksteini <77672665+kksteini@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:18:59 +0000 Subject: [PATCH 008/173] feat(application-plc): Procuration (#15945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(application-plc): Procuration * let→const --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../parliamentary-list-creation.module.ts | 10 ++++++- .../parliamentary-list-creation.service.ts | 30 ++++++++++++++++++- .../src/dataProviders/index.ts | 4 +++ .../src/forms/Draft.ts | 6 ++-- .../src/forms/Prerequisites.ts | 15 +++++----- .../src/lib/createCollectionTemplate.ts | 7 ++++- .../src/lib/signature-collection.service.ts | 12 ++------ 7 files changed, 62 insertions(+), 22 deletions(-) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.module.ts index 111c2917eabb..73ac8614bb22 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.module.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.module.ts @@ -10,6 +10,10 @@ import { BaseTemplateAPIModuleConfig } from '../../../../types' // Here you import your module service import { ParliamentaryListCreationService } from './parliamentary-list-creation.service' import { SignatureCollectionClientModule } from '@island.is/clients/signature-collection' +import { + NationalRegistryClientModule, + NationalRegistryClientService, +} from '@island.is/clients/national-registry-v2' export class ParliamentaryListCreationModule { static register(config: BaseTemplateAPIModuleConfig): DynamicModule { @@ -18,8 +22,12 @@ export class ParliamentaryListCreationModule { imports: [ SharedTemplateAPIModule.register(config), SignatureCollectionClientModule, + NationalRegistryClientModule, + ], + providers: [ + ParliamentaryListCreationService, + NationalRegistryClientService, ], - providers: [ParliamentaryListCreationService], exports: [ParliamentaryListCreationService], } } 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 5113581ddec9..22eb6250e522 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 @@ -17,6 +17,9 @@ import { LOGGER_PROVIDER } from '@island.is/logging' import { CollectionType } from '@island.is/clients/signature-collection' import { CreateListSchema } from '@island.is/application/templates/signature-collection/parliamentary-list-creation' import { FetchError } from '@island.is/clients/middlewares' +import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { isCompany } from 'kennitala' +import { coreErrorMessages } from '@island.is/application/core' @Injectable() export class ParliamentaryListCreationService extends BaseTemplateApiService { @@ -24,6 +27,7 @@ export class ParliamentaryListCreationService extends BaseTemplateApiService { @Inject(LOGGER_PROVIDER) private logger: Logger, private readonly _sharedTemplateAPIService: SharedTemplateApiService, private signatureCollectionClientService: SignatureCollectionClientService, + private nationalRegisryClientService: NationalRegistryClientService, ) { super(ApplicationTypes.PARLIAMENTARY_LIST_CREATION) } @@ -53,13 +57,37 @@ export class ParliamentaryListCreationService extends BaseTemplateApiService { return currentCollection } + async parliamentaryIdentity({ auth }: TemplateApiModuleActionProps) { + const contactNationalId = isCompany(auth.nationalId) + ? auth.actor?.nationalId ?? auth.nationalId + : auth.nationalId + + const identity = await this.nationalRegisryClientService.getIndividual( + contactNationalId, + ) + + if (!identity) { + throw new TemplateApiError( + coreErrorMessages.nationalIdNotFoundInNationalRegistrySummary, + 500, + ) + } + + return identity + } + async submit({ application, auth }: TemplateApiModuleActionProps) { const answers = application.answers as CreateListSchema const parliamentaryCollection = application.externalData .parliamentaryCollection.data as Collection const input: CreateParliamentaryCandidacyInput = { - owner: answers.applicant, + owner: { + ...answers.applicant, + nationalId: application?.applicantActors?.[0] + ? application.applicant + : answers.applicant.nationalId, + }, agents: (answers.managers ?? []) .map((manager) => ({ nationalId: manager.manager.nationalId, diff --git a/libs/application/templates/signature-collection/parliamentary-list-creation/src/dataProviders/index.ts b/libs/application/templates/signature-collection/parliamentary-list-creation/src/dataProviders/index.ts index 2aa7077f3479..f3155769dfb3 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-creation/src/dataProviders/index.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-creation/src/dataProviders/index.ts @@ -7,3 +7,7 @@ export const CandidateApi = defineTemplateApi({ export const ParliamentaryCollectionApi = defineTemplateApi({ action: 'parliamentaryCollection', }) + +export const ParliamentaryIdentityApi = defineTemplateApi({ + action: 'parliamentaryIdentity', +}) diff --git a/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Draft.ts b/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Draft.ts index dbcc301df618..0dc9108b06ca 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Draft.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Draft.ts @@ -94,7 +94,7 @@ export const Draft: Form = buildForm({ width: 'half', readOnly: true, defaultValue: ({ externalData }: Application) => { - return externalData.nationalRegistry?.data.fullName + return externalData.parliamentaryIdentity?.data.fullName }, }), buildTextField({ @@ -103,7 +103,9 @@ export const Draft: Form = buildForm({ width: 'half', readOnly: true, defaultValue: (application: Application) => - formatNationalId(application.applicant), + application?.applicantActors[0] + ? formatNationalId(application?.applicantActors[0]) + : formatNationalId(application.applicant), }), buildPhoneField({ id: 'applicant.phone', diff --git a/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Prerequisites.ts b/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Prerequisites.ts index cf5183b4e9b1..5af0d1ae58fb 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Prerequisites.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Prerequisites.ts @@ -17,7 +17,11 @@ import { import { m } from '../lib/messages' import Logo from '../../assets/Logo' -import { ParliamentaryCollectionApi, CandidateApi } from '../dataProviders' +import { + ParliamentaryCollectionApi, + CandidateApi, + ParliamentaryIdentityApi, +} from '../dataProviders' export const Prerequisites: Form = buildForm({ id: 'CreateListPrerequisites', @@ -73,17 +77,12 @@ export const Prerequisites: Form = buildForm({ subTitle: m.userProfileProviderSubtitle, }), buildDataProviderItem({ - provider: NationalRegistryUserApi, - title: m.nationalRegistryProviderTitle, - subTitle: m.nationalRegistryProviderSubtitle, - }), - buildDataProviderItem({ - //provider: TODO: Add providers needed for creating collection, + provider: ParliamentaryCollectionApi, title: '', subTitle: '', }), buildDataProviderItem({ - provider: ParliamentaryCollectionApi, + provider: ParliamentaryIdentityApi, title: '', subTitle: '', }), diff --git a/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/createCollectionTemplate.ts b/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/createCollectionTemplate.ts index 42d7c9327ba5..ded47fc406ed 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/createCollectionTemplate.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/createCollectionTemplate.ts @@ -17,7 +17,11 @@ import { dataSchema } from './dataSchema' import { m } from './messages' import { EphemeralStateLifeCycle } from '@island.is/application/core' import { Features } from '@island.is/feature-flags' -import { ParliamentaryCollectionApi, CandidateApi } from '../dataProviders' +import { + ParliamentaryCollectionApi, + CandidateApi, + ParliamentaryIdentityApi, +} from '../dataProviders' import { AuthDelegationType } from '@island.is/shared/types' const WeekLifeCycle: StateLifeCycle = { @@ -76,6 +80,7 @@ const createListTemplate: ApplicationTemplate< UserProfileApi, CandidateApi, ParliamentaryCollectionApi, + ParliamentaryIdentityApi, ], }, ], diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.ts index d1bd4a77d4c4..c182467a626b 100644 --- a/libs/clients/signature-collection/src/lib/signature-collection.service.ts +++ b/libs/clients/signature-collection/src/lib/signature-collection.service.ts @@ -150,15 +150,9 @@ export class SignatureCollectionClientService { throw new Error('Collection is not open') } - // check if user is sending in their own nationalId - if (owner.nationalId.replace(/\D/g, '') !== auth.nationalId) { - // TODO: create ApplicationTemplateError - throw new Error('NationalId does not match') - } - // check if user is already owner of lists - - const { canCreate, isOwner, name, partyBallotLetterInfo } = - await this.getSignee(auth) + const { canCreate, isOwner, partyBallotLetterInfo } = await this.getSignee( + auth, + ) if (!canCreate || isOwner) { // TODO: create ApplicationTemplateError throw new Error('User is already owner of lists') From 2a36e0aeacd3533b0b621e5d041084bf9fe2e71c Mon Sep 17 00:00:00 2001 From: veronikasif <54938148+veronikasif@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:40:47 +0000 Subject: [PATCH 009/173] feat(income-plan): Incorrect monthly income when changing between montly and yearly income (#15953) * Add updateValueObj to monthly income * [TS-903] Incorrect monthly income when changing between montly and yearly income --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/forms/IncomePlanForm.ts | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 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 8020c80820d1..43610292f470 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 @@ -171,13 +171,25 @@ export const IncomePlanForm: Form = buildForm({ type: 'number', displayInTable: false, currency: true, - defaultValue: (_, activeField) => { - if (activeField?.incomePerYear) { - return Math.round( - Number(activeField?.incomePerYear) / 12, - ).toString() - } - return '' + updateValueObj: { + valueModifier: (activeField) => { + const unevenAndEmploymentIncome = + activeField?.unevenIncomePerYear?.[0] !== YES || + (activeField?.incomeCategory !== INCOME && + activeField?.unevenIncomePerYear?.[0] === YES) + + if ( + activeField?.income === RatioType.MONTHLY && + activeField?.currency !== ISK && + unevenAndEmploymentIncome + ) { + return Math.round( + Number(activeField?.incomePerYear) / 12, + ).toString() + } + return undefined + }, + watchValues: 'income', }, suffix: '', condition: (_, activeField) => { @@ -200,13 +212,25 @@ export const IncomePlanForm: Form = buildForm({ type: 'number', displayInTable: false, currency: true, - defaultValue: (_, activeField) => { - if (activeField?.incomePerYear) { - return Math.round( - Number(activeField?.incomePerYear) / 12, - ).toString() - } - return '' + updateValueObj: { + valueModifier: (activeField) => { + const unevenAndEmploymentIncome = + activeField?.unevenIncomePerYear?.[0] !== YES || + (activeField?.incomeCategory !== INCOME && + activeField?.unevenIncomePerYear?.[0] === YES) + + if ( + activeField?.income === RatioType.MONTHLY && + activeField?.currency === ISK && + unevenAndEmploymentIncome + ) { + return Math.round( + Number(activeField?.incomePerYear) / 12, + ).toString() + } + return undefined + }, + watchValues: 'income', }, suffix: '', condition: (_, activeField) => { From 76faccdbef42e5acb6bc88eb3af129aa704fa517 Mon Sep 17 00:00:00 2001 From: veronikasif <54938148+veronikasif@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:48:32 +0000 Subject: [PATCH 010/173] feat(income-plan): Don't allow negative numbers or 0 in income plan (#15982) * [TS-911] Don't allow negative numbers or 0 in income plan * Refactor the validation logic to reduce duplication --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/lib/dataSchema.ts | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/lib/dataSchema.ts b/libs/application/templates/social-insurance-administration/income-plan/src/lib/dataSchema.ts index bf20588b6047..b2a8d5890a4d 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/lib/dataSchema.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/lib/dataSchema.ts @@ -1,3 +1,4 @@ +import { coreErrorMessages } from '@island.is/application/core' import { z } from 'zod' import { INCOME, ISK, RatioType, YES } from './constants' import { errorMessages } from './messages' @@ -31,7 +32,9 @@ export const dataSchema = z.object({ }) .refine( ({ income, incomePerYear }) => - income === RatioType.YEARLY ? !!incomePerYear : true, + income === RatioType.YEARLY + ? !!incomePerYear && Number(incomePerYear) > 0 + : true, { path: ['incomePerYear'], }, @@ -51,7 +54,7 @@ export const dataSchema = z.object({ return income === RatioType.MONTHLY && currency === ISK && unevenAndEmploymentIncome - ? !!equalIncomePerMonth + ? !!equalIncomePerMonth && Number(equalIncomePerMonth) > 0 : true }, { @@ -73,7 +76,8 @@ export const dataSchema = z.object({ return income === RatioType.MONTHLY && currency !== ISK && unevenAndEmploymentIncome - ? !!equalForeignIncomePerMonth + ? !!equalForeignIncomePerMonth && + Number(equalForeignIncomePerMonth) > 0 : true }, { @@ -104,7 +108,39 @@ export const dataSchema = z.object({ path: ['incomePerYear'], params: errorMessages.monthsRequired, }, - ), + ) + .superRefine((incomePlanTable, ctx) => { + if ( + incomePlanTable.income === RatioType.MONTHLY && + incomePlanTable?.incomeCategory === INCOME && + incomePlanTable.unevenIncomePerYear?.[0] === YES + ) { + const months = { + january: incomePlanTable.january, + february: incomePlanTable.february, + march: incomePlanTable.march, + april: incomePlanTable.april, + may: incomePlanTable.may, + june: incomePlanTable.june, + july: incomePlanTable.july, + august: incomePlanTable.august, + september: incomePlanTable.september, + october: incomePlanTable.october, + november: incomePlanTable.november, + december: incomePlanTable.december, + } + + Object.entries(months).forEach(([key, value]) => { + if (value && !(Number(value) > 0)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [key], + params: coreErrorMessages.defaultError, + }) + } + }) + } + }), ) .refine((i) => i === undefined || i.length > 0, { params: errorMessages.incomePlanRequired, From 037c5f36e4fc78db6f22e074ba42a0fa966d7a3a Mon Sep 17 00:00:00 2001 From: Ylfa <55542991+ylfahfa@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:58:14 +0000 Subject: [PATCH 011/173] fix(income-plan): Hide IKR when foreign income (#15987) * hide ikr when foreign income * chore: nx format:write update dirty files * format --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/forms/IncomePlanForm.ts | 18 ++++++++++++++++-- .../income-plan/src/lib/constants.ts | 6 ++++++ 2 files changed, 22 insertions(+), 2 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 43610292f470..e48b985bcfe9 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 @@ -21,8 +21,12 @@ import { formatCurrencyWithoutSuffix } from '@island.is/application/ui-component import { buildFormConclusionSection } from '@island.is/application/ui-forms' import isEmpty from 'lodash/isEmpty' import { + DIVIDENDS_IN_FOREIGN_BANKS, FOREIGN_BASIC_PENSION, + FOREIGN_INCOME, + FOREIGN_PENSION, INCOME, + INTEREST_ON_DEPOSITS_IN_FOREIGN_BANKS, ISK, RatioType, YES, @@ -127,7 +131,12 @@ export const IncomePlanForm: Form = buildForm({ updateValueObj: { valueModifier: (activeField) => { const defaultCurrency = - activeField?.incomeType === FOREIGN_BASIC_PENSION + activeField?.incomeType === FOREIGN_BASIC_PENSION || + activeField?.incomeType === FOREIGN_PENSION || + activeField?.incomeType === FOREIGN_INCOME || + activeField?.incomeType === + INTEREST_ON_DEPOSITS_IN_FOREIGN_BANKS || + activeField?.incomeType === DIVIDENDS_IN_FOREIGN_BANKS ? '' : ISK @@ -141,7 +150,12 @@ export const IncomePlanForm: Form = buildForm({ ) const hideISKCurrency = - activeField?.incomeType === FOREIGN_BASIC_PENSION + activeField?.incomeType === FOREIGN_BASIC_PENSION || + activeField?.incomeType === FOREIGN_PENSION || + activeField?.incomeType === FOREIGN_INCOME || + activeField?.incomeType === + INTEREST_ON_DEPOSITS_IN_FOREIGN_BANKS || + activeField?.incomeType === DIVIDENDS_IN_FOREIGN_BANKS ? ISK : '' diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts b/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts index 3759a2d13ec5..a87cc7cbb9f7 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/lib/constants.ts @@ -1,6 +1,12 @@ export const YES = 'yes' export const NO = 'no' export const FOREIGN_BASIC_PENSION = 'Erlendur grunnlífeyrir' +export const FOREIGN_PENSION = 'Erlendur lífeyrir' +export const FOREIGN_INCOME = 'Erlendar tekjur' +export const INTEREST_ON_DEPOSITS_IN_FOREIGN_BANKS = + 'Vextir af innstæðum í erlendum bönkum' +export const DIVIDENDS_IN_FOREIGN_BANKS = + 'Arður af hlutabréfa eign í erlendum bönkum' export const ISK = 'IKR' export const INCOME = 'Atvinnutekjur' export const INCOME_PLANS_CLOSED = 'INCOME_PLANS_CLOSED' From 2c7c9c0abce419dbd948c3f4909e01d44dfe3678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20M=C3=A1r=20Atlason?= <54210288+saevarma@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:34:05 +0000 Subject: [PATCH 012/173] fix(user-profile): Check if nationalId+deviceToken pair exists and only create if not. (#15990) * Use upsert instead of create to stop unique constraint errors * Upsert is not converting properties to propper db column names with underscore. So moving to a simple findOne and create if not found. * Fix addDeviceToken test --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/app/user-profile/e2e/userProfile.spec.ts | 10 ++++++---- .../src/app/user-profile/userProfile.controller.ts | 5 ++++- .../src/app/user-profile/userProfile.service.ts | 8 ++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/services/user-profile/src/app/user-profile/e2e/userProfile.spec.ts b/apps/services/user-profile/src/app/user-profile/e2e/userProfile.spec.ts index e27ef686e3b8..67a30b165a11 100644 --- a/apps/services/user-profile/src/app/user-profile/e2e/userProfile.spec.ts +++ b/apps/services/user-profile/src/app/user-profile/e2e/userProfile.spec.ts @@ -816,21 +816,23 @@ describe('User profile API', () => { ) }) - it('POST /userProfile/{nationalId}/device-tokens duplicate token should return 400 bad request', async () => { + it('POST /userProfile/{nationalId}/device-tokens duplicate token should return the existing token', async () => { // create it - await request(app.getHttpServer()) + const res1 = await request(app.getHttpServer()) .post(`/userProfile/${mockProfile.nationalId}/device-tokens`) .send({ deviceToken: mockDeviceToken.deviceToken, }) .expect(201) // try to create same again - await request(app.getHttpServer()) + const res2 = await request(app.getHttpServer()) .post(`/userProfile/${mockProfile.nationalId}/device-tokens`) .send({ deviceToken: mockDeviceToken.deviceToken, }) - .expect(400) + .expect(201) + + expect(res1.body).toEqual(res2.body) }) it('POST /userProfile/{nationalId}/device-tokens with missing payload should 400 bad request', async () => { diff --git a/apps/services/user-profile/src/app/user-profile/userProfile.controller.ts b/apps/services/user-profile/src/app/user-profile/userProfile.controller.ts index cb39b058a4eb..91865aa2e0b1 100644 --- a/apps/services/user-profile/src/app/user-profile/userProfile.controller.ts +++ b/apps/services/user-profile/src/app/user-profile/userProfile.controller.ts @@ -458,7 +458,10 @@ export class UserProfileController { } else { // findOrCreateUserProfile for edge cases - fragmented onboarding await this.findOrCreateUserProfile(nationalId, user) - return await this.userProfileService.addDeviceToken(body, user) + // The behaviour of returning the token if it already exists is not following API Design Guide + // It should respond with 303 See Other and a Location header to the existing resource + // But as the v1 of the user profile is not following this, we will keep the same behaviour. + return this.userProfileService.addDeviceToken(body, user) } } diff --git a/apps/services/user-profile/src/app/user-profile/userProfile.service.ts b/apps/services/user-profile/src/app/user-profile/userProfile.service.ts index 32abceae163e..3010c9cc0937 100644 --- a/apps/services/user-profile/src/app/user-profile/userProfile.service.ts +++ b/apps/services/user-profile/src/app/user-profile/userProfile.service.ts @@ -63,6 +63,14 @@ export class UserProfileService { async addDeviceToken(body: DeviceTokenDto, user: User) { try { + const token = await this.userDeviceTokensModel.findOne({ + where: { nationalId: user.nationalId, deviceToken: body.deviceToken }, + }) + + if (token) { + return token + } + return await this.userDeviceTokensModel.create({ ...body, nationalId: user.nationalId, From 3b8183d9118f513b29d635e1c90b8689f0875203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:17:33 +0000 Subject: [PATCH 013/173] fix(cms): Latest Generic List Items - Mark seeMorePage field as nullable (#15997) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/cms/src/lib/models/latestGenericListItems.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cms/src/lib/models/latestGenericListItems.model.ts b/libs/cms/src/lib/models/latestGenericListItems.model.ts index 56a0672ca3cd..351712ccbbc6 100644 --- a/libs/cms/src/lib/models/latestGenericListItems.model.ts +++ b/libs/cms/src/lib/models/latestGenericListItems.model.ts @@ -23,7 +23,7 @@ export class LatestGenericListItems { @CacheField(() => GenericList, { nullable: true }) genericList?: GenericList | null - @CacheField(() => PageUnion) + @CacheField(() => PageUnion, { nullable: true }) seeMorePage?: typeof PageUnion | null @Field() From e09340c8db2144e313398731f61be08e00dfaf43 Mon Sep 17 00:00:00 2001 From: Ylfa <55542991+ylfahfa@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:07:02 +0000 Subject: [PATCH 014/173] feat(income-plan): Only allow one income plan in draft (#15998) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/lib/IncomePlanTemplate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/lib/IncomePlanTemplate.ts b/libs/application/templates/social-insurance-administration/income-plan/src/lib/IncomePlanTemplate.ts index 6d198db30f5d..00b1d7fa35e9 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/lib/IncomePlanTemplate.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/lib/IncomePlanTemplate.ts @@ -61,6 +61,7 @@ const IncomePlanTemplate: ApplicationTemplate< featureFlag: Features.IncomePlanEnabled, translationNamespaces: ApplicationConfigurations.IncomePlan.translation, dataSchema, + allowMultipleApplicationsInDraft: false, newApplicationButtonLabel: historyMessages.newIncomePlanButtonLabel, applicationText: historyMessages.incomePlanPageTitle, stateMachineConfig: { 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 015/173] 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 016/173] 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 017/173] 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 018/173] 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 019/173] 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 020/173] 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 021/173] 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 022/173] 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 023/173] 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 024/173] 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 025/173] 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 026/173] 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 027/173] 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 028/173] 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 029/173] 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 030/173] 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 031/173] 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 032/173] 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 033/173] 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 034/173] 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 035/173] 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 036/173] 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 037/173] 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 038/173] 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 039/173] 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 040/173] 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 041/173] 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', From 7572adf1811a038dec2c6e1dc5a1425c544f1900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Mon, 16 Sep 2024 20:54:31 +0000 Subject: [PATCH 042/173] feat(j-s): Civil Demands (#16010) * Adds civil demands to traffic violation indictments * Handles case strings in the backend only * Renames the case file interceptor * Adds civil demands to indictment pdf --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../app/modules/backend/backend.service.ts | 13 +--- .../app/modules/case/dto/updateCase.input.ts | 5 ++ .../src/app/modules/case/models/case.model.ts | 3 + ...240909194458-rename-explanatory-comment.js | 50 +++++++++++++ .../src/app/formatters/indictmentPdf.ts | 17 +++-- .../backend/src/app/messages/pdfIndictment.ts | 5 ++ .../src/app/modules/case/case.controller.ts | 14 +++- .../src/app/modules/case/case.module.ts | 9 +-- .../src/app/modules/case/case.service.ts | 70 ++++++++++--------- .../app/modules/case/dto/updateCase.dto.ts | 5 ++ .../src/app/modules/case/guards/rolesRules.ts | 1 + .../case/interceptors/case.interceptor.ts | 38 ++++++++++ .../case/interceptors/caseFile.interceptor.ts | 58 --------------- .../case/interceptors/caseList.interceptor.ts | 6 +- .../caseOriginalAncestor.interceptor.ts | 10 +-- .../completedAppealAccessed.interceptor.ts | 17 +++-- .../limitedAccessCaseFile.interceptor.ts | 36 ++++++++++ .../modules/case/internalCase.controller.ts | 12 +++- .../app/modules/case/internalCase.service.ts | 45 +++++------- .../case/limitedAccessCase.controller.ts | 11 ++- .../modules/case/limitedAccessCase.service.ts | 13 ++-- .../src/app/modules/case/models/case.model.ts | 10 +-- ...ryComment.model.ts => caseString.model.ts} | 35 +++++----- .../case/test/caseController/update.spec.ts | 36 +++++++--- .../case/test/createTestingCaseModule.ts | 10 +-- .../internalCaseController/archive.spec.ts | 49 ++++++------- .../src/app/modules/event/event.service.ts | 16 ++--- .../modules/file/guards/caseFileCategory.ts | 56 ++++++++++++++- .../guards/limitedAccessViewCaseFile.guard.ts | 44 +++--------- .../backend/src/app/modules/file/index.ts | 5 +- .../src/app/modules/cases/case.service.ts | 2 +- .../src/components/FormProvider/case.graphql | 1 + .../Indictment/Indictment.strings.ts | 18 +++++ .../Indictments/Indictment/Indictment.tsx | 43 +++++++++++- .../judicial-system/web/src/utils/validate.ts | 2 +- libs/judicial-system/types/src/index.ts | 2 +- .../src/lib/{comment.ts => caseString.ts} | 3 +- 37 files changed, 477 insertions(+), 293 deletions(-) create mode 100644 apps/judicial-system/backend/migrations/20240909194458-rename-explanatory-comment.js create mode 100644 apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts delete mode 100644 apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts create mode 100644 apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts rename apps/judicial-system/backend/src/app/modules/case/models/{explanatoryComment.model.ts => caseString.model.ts} (53%) rename libs/judicial-system/types/src/lib/{comment.ts => caseString.ts} (56%) diff --git a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts index 95975ec85792..0dc40cde6cef 100644 --- a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts +++ b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts @@ -6,12 +6,7 @@ import { Inject, Injectable } from '@nestjs/common' import { type ConfigType } from '@island.is/nest/config' import { ProblemError } from '@island.is/nest/problem' -import { - CommentType, - DateType, - type User, - UserRole, -} from '@island.is/judicial-system/types' +import { DateType, type User, UserRole } from '@island.is/judicial-system/types' import { Case, @@ -144,7 +139,6 @@ export class BackendService extends DataSource<{ req: Request }> { private caseTransformer(data: unknown): Case { const theCase = data as Case & { dateLogs?: { dateType: DateType; date: string }[] - explanatoryComments?: { commentType: CommentType; comment: string }[] } return { @@ -155,11 +149,6 @@ export class BackendService extends DataSource<{ req: Request }> { courtDate: theCase.dateLogs?.find( (dateLog) => dateLog.dateType === DateType.COURT_DATE, ), - postponedIndefinitelyExplanation: theCase.explanatoryComments?.find( - (comment) => - comment.commentType === - CommentType.POSTPONED_INDEFINITELY_EXPLANATION, - )?.comment, } } diff --git a/apps/judicial-system/api/src/app/modules/case/dto/updateCase.input.ts b/apps/judicial-system/api/src/app/modules/case/dto/updateCase.input.ts index 65059587bf76..0e506a67f685 100644 --- a/apps/judicial-system/api/src/app/modules/case/dto/updateCase.input.ts +++ b/apps/judicial-system/api/src/app/modules/case/dto/updateCase.input.ts @@ -506,4 +506,9 @@ export class UpdateCaseInput { @IsOptional() @Field(() => ID, { nullable: true }) readonly mergeCaseId?: string + + @Allow() + @IsOptional() + @Field(() => String, { nullable: true }) + readonly civilDemands?: string } diff --git a/apps/judicial-system/api/src/app/modules/case/models/case.model.ts b/apps/judicial-system/api/src/app/modules/case/models/case.model.ts index 247a8cc98056..0f09dd9b6274 100644 --- a/apps/judicial-system/api/src/app/modules/case/models/case.model.ts +++ b/apps/judicial-system/api/src/app/modules/case/models/case.model.ts @@ -453,4 +453,7 @@ export class Case { @Field(() => [Case], { nullable: true }) readonly mergedCases?: Case[] + + @Field(() => String, { nullable: true }) + readonly civilDemands?: string } diff --git a/apps/judicial-system/backend/migrations/20240909194458-rename-explanatory-comment.js b/apps/judicial-system/backend/migrations/20240909194458-rename-explanatory-comment.js new file mode 100644 index 000000000000..8235e54c67c7 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240909194458-rename-explanatory-comment.js @@ -0,0 +1,50 @@ +'use strict' + +module.exports = { + async up(queryInterface) { + return queryInterface.sequelize.transaction(async (transaction) => + queryInterface + .renameTable('explanatory_comment', 'case_string', { + transaction, + }) + .then(() => + Promise.all([ + queryInterface.renameColumn( + 'case_string', + 'comment_type', + 'string_type', + { transaction }, + ), + queryInterface.renameColumn('case_string', 'comment', 'value', { + transaction, + }), + ]), + ), + ) + }, + + async down(queryInterface) { + return queryInterface.sequelize.transaction(async (transaction) => + queryInterface + .renameTable('case_string', 'explanatory_comment', { + transaction, + }) + .then(() => + Promise.all([ + queryInterface.renameColumn( + 'explanatory_comment', + 'string_type', + 'comment_type', + { transaction }, + ), + queryInterface.renameColumn( + 'explanatory_comment', + 'value', + 'comment', + { transaction }, + ), + ]), + ), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts index 5ab70b81c81a..0279dbb208f9 100644 --- a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts @@ -8,6 +8,7 @@ import { formatDate, lowercase } from '@island.is/judicial-system/formatters' import { nowFactory } from '../factories' import { indictment } from '../messages' import { Case } from '../modules/case' +import { CaseString } from '../modules/case/models/caseString.model' import { addEmptyLines, addGiganticHeading, @@ -69,10 +70,7 @@ export const createIndictment = async ( doc.on('data', (chunk) => sinc.push(chunk)) - const title = formatMessage(indictment.title) - const heading = formatMessage(indictment.heading) - - setTitle(doc, title) + setTitle(doc, formatMessage(indictment.title)) if (confirmation) { addIndictmentConfirmation(doc, confirmation) @@ -80,7 +78,7 @@ export const createIndictment = async ( addEmptyLines(doc, 6, doc.page.margins.left) - addGiganticHeading(doc, heading, 'Times-Roman') + addGiganticHeading(doc, formatMessage(indictment.heading), 'Times-Roman') addNormalPlusText(doc, ' ') setLineCap(2) addNormalPlusText(doc, theCase.indictmentIntroduction ?? '') @@ -103,6 +101,15 @@ export const createIndictment = async ( addEmptyLines(doc, 2) addNormalPlusJustifiedText(doc, theCase.demands ?? '') + + const civilDemands = CaseString.civilDemands(theCase.caseStrings) + + if (civilDemands) { + addEmptyLines(doc, 2) + addNormalPlusText(doc, formatMessage(indictment.civilDemandsHeading)) + addNormalPlusJustifiedText(doc, civilDemands) + } + addEmptyLines(doc, 2) addNormalPlusCenteredText( doc, diff --git a/apps/judicial-system/backend/src/app/messages/pdfIndictment.ts b/apps/judicial-system/backend/src/app/messages/pdfIndictment.ts index aa824eff2fc6..e74344133512 100644 --- a/apps/judicial-system/backend/src/app/messages/pdfIndictment.ts +++ b/apps/judicial-system/backend/src/app/messages/pdfIndictment.ts @@ -17,4 +17,9 @@ export const indictment = defineMessages({ defaultMessage: 'ÁKÆRA', description: 'Notaður sem heading á ákæru PDF', }, + civilDemandsHeading: { + id: 'judicial.system.backend:pdf.indictment.civil_demands_heading', + defaultMessage: 'Einkaréttarkrafa:', + description: 'Notaður sem titill á einkaréttarkröfu í ákæru PDF', + }, }) diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts index 7b0da96f91d1..9efb4338c21f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts @@ -99,6 +99,10 @@ import { prosecutorUpdateRule, publicProsecutorStaffUpdateRule, } from './guards/rolesRules' +import { + CaseInterceptor, + CasesInterceptor, +} from './interceptors/case.interceptor' import { CaseListInterceptor } from './interceptors/caseList.interceptor' import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' import { Case } from './models/case.model' @@ -140,6 +144,7 @@ export class CaseController { @UseGuards(JwtAuthGuard, RolesGuard) @RolesRules(prosecutorRule, prosecutorRepresentativeRule) + @UseInterceptors(CaseInterceptor) @Post('case') @ApiCreatedResponse({ type: Case, description: 'Creates a new case' }) async create( @@ -167,6 +172,7 @@ export class CaseController { courtOfAppealsAssistantUpdateRule, publicProsecutorStaffUpdateRule, ) + @UseInterceptors(CaseInterceptor) @Patch('case/:caseId') @ApiOkResponse({ type: Case, description: 'Updates an existing case' }) async update( @@ -284,6 +290,7 @@ export class CaseController { courtOfAppealsRegistrarTransitionRule, courtOfAppealsAssistantTransitionRule, ) + @UseInterceptors(CaseInterceptor) @Patch('case/:caseId/state') @ApiOkResponse({ type: Case, @@ -438,13 +445,13 @@ export class CaseController { prisonSystemStaffRule, defenderRule, ) + @UseInterceptors(CaseListInterceptor) @Get('cases') @ApiOkResponse({ type: Case, isArray: true, description: 'Gets all existing cases', }) - @UseInterceptors(CaseListInterceptor) getAll(@CurrentHttpUser() user: User): Promise { this.logger.debug('Getting all cases') @@ -463,9 +470,9 @@ export class CaseController { courtOfAppealsRegistrarRule, courtOfAppealsAssistantRule, ) + @UseInterceptors(CompletedAppealAccessedInterceptor, CaseInterceptor) @Get('case/:caseId') @ApiOkResponse({ type: Case, description: 'Gets an existing case' }) - @UseInterceptors(CompletedAppealAccessedInterceptor) getById(@Param('caseId') caseId: string, @CurrentCase() theCase: Case): Case { this.logger.debug(`Getting case ${caseId} by id`) @@ -478,6 +485,7 @@ export class CaseController { districtCourtRegistrarRule, districtCourtAssistantRule, ) + @UseInterceptors(CasesInterceptor) @Get('case/:caseId/connectedCases') @ApiOkResponse({ type: [Case], description: 'Gets all connected cases' }) async getConnectedCases( @@ -872,6 +880,7 @@ export class CaseController { CaseReadGuard, ) @RolesRules(prosecutorRule) + @UseInterceptors(CaseInterceptor) @Post('case/:caseId/extend') @ApiCreatedResponse({ type: Case, @@ -901,6 +910,7 @@ export class CaseController { districtCourtRegistrarRule, districtCourtAssistantRule, ) + @UseInterceptors(CaseInterceptor) @Post('case/:caseId/court') @ApiCreatedResponse({ type: Case, diff --git a/apps/judicial-system/backend/src/app/modules/case/case.module.ts b/apps/judicial-system/backend/src/app/modules/case/case.module.ts index 40cd18358f20..10d099b3068b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.module.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.module.ts @@ -19,8 +19,8 @@ import { } from '../index' import { Case } from './models/case.model' import { CaseArchive } from './models/caseArchive.model' +import { CaseString } from './models/caseString.model' import { DateLog } from './models/dateLog.model' -import { ExplanatoryComment } from './models/explanatoryComment.model' import { CaseController } from './case.controller' import { CaseService } from './case.service' import { InternalCaseController } from './internalCase.controller' @@ -43,12 +43,7 @@ import { PdfService } from './pdf.service' forwardRef(() => EventModule), forwardRef(() => PoliceModule), forwardRef(() => EventLogModule), - SequelizeModule.forFeature([ - Case, - CaseArchive, - DateLog, - ExplanatoryComment, - ]), + SequelizeModule.forFeature([Case, CaseArchive, DateLog, CaseString]), ], providers: [ CaseService, 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 f1ac7a7078fc..ddd76c7772d4 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 @@ -37,7 +37,6 @@ import { CaseState, CaseTransition, CaseType, - CommentType, DateType, EventType, isCompletedCase, @@ -45,6 +44,7 @@ import { isRequestCase, isTrafficViolationCase, NotificationType, + StringType, UserRole, } from '@island.is/judicial-system/types' @@ -66,8 +66,8 @@ import { User } from '../user' import { CreateCaseDto } from './dto/createCase.dto' import { getCasesQueryFilter } from './filters/cases.filter' import { Case } from './models/case.model' +import { CaseString } from './models/caseString.model' import { DateLog } from './models/dateLog.model' -import { ExplanatoryComment } from './models/explanatoryComment.model' import { SignatureConfirmationResponse } from './models/signatureConfirmation.response' import { transitionCase } from './state/case.state' import { caseModuleConfig } from './case.config' @@ -178,6 +178,7 @@ export interface UpdateCase indictmentReturnedExplanation?: string | null indictmentDeniedExplanation?: string | null indictmentHash?: string | null + civilDemands?: string | null } type DateLogKeys = keyof Pick @@ -187,19 +188,20 @@ const dateLogTypes: Record = { courtDate: DateType.COURT_DATE, } -type ExplanatoryCommentKeys = keyof Pick< +type CaseStringKeys = keyof Pick< UpdateCase, - 'postponedIndefinitelyExplanation' + 'postponedIndefinitelyExplanation' | 'civilDemands' > -const explanatoryCommentTypes: Record = { +const caseStringTypes: Record = { postponedIndefinitelyExplanation: - CommentType.POSTPONED_INDEFINITELY_EXPLANATION, + StringType.POSTPONED_INDEFINITELY_EXPLANATION, + civilDemands: StringType.CIVIL_DEMANDS, } const eventTypes = Object.values(EventType) const dateTypes = Object.values(DateType) -const commentTypes = Object.values(CommentType) +const stringTypes = Object.values(StringType) export const include: Includeable[] = [ { model: Institution, as: 'prosecutorsOffice' }, @@ -293,10 +295,10 @@ export const include: Includeable[] = [ where: { dateType: { [Op.in]: dateTypes } }, }, { - model: ExplanatoryComment, - as: 'explanatoryComments', + model: CaseString, + as: 'caseStrings', required: false, - where: { commentType: { [Op.in]: commentTypes } }, + where: { stringType: { [Op.in]: stringTypes } }, }, { model: Notification, as: 'notifications' }, { model: Case, as: 'mergeCase' }, @@ -374,10 +376,10 @@ export const caseListInclude: Includeable[] = [ where: { dateType: { [Op.in]: dateTypes } }, }, { - model: ExplanatoryComment, - as: 'explanatoryComments', + model: CaseString, + as: 'caseStrings', required: false, - where: { commentType: { [Op.in]: commentTypes } }, + where: { stringType: { [Op.in]: stringTypes } }, }, { model: EventLog, @@ -400,8 +402,8 @@ export class CaseService { @InjectConnection() private readonly sequelize: Sequelize, @InjectModel(Case) private readonly caseModel: typeof Case, @InjectModel(DateLog) private readonly dateLogModel: typeof DateLog, - @InjectModel(ExplanatoryComment) - private readonly explanatoryCommentModel: typeof ExplanatoryComment, + @InjectModel(CaseString) + private readonly caseStringModel: typeof CaseString, @Inject(caseModuleConfig.KEY) private readonly config: ConfigType, private readonly defendantService: DefendantService, @@ -1468,39 +1470,39 @@ export class CaseService { update: UpdateCase, transaction: Transaction, ) { - // Iterate over all known explanatory comment types - for (const key in explanatoryCommentTypes) { - const commentKey = key as ExplanatoryCommentKeys - const updateComment = update[commentKey] + // Iterate over all known case string types + for (const key in caseStringTypes) { + const caseStringKey = key as CaseStringKeys + const updateCaseString = update[caseStringKey] - if (updateComment !== undefined) { - const commentType = explanatoryCommentTypes[commentKey] + if (updateCaseString !== undefined) { + const stringType = caseStringTypes[caseStringKey] - const comment = await this.explanatoryCommentModel.findOne({ - where: { caseId: theCase.id, commentType }, + const caseString = await this.caseStringModel.findOne({ + where: { caseId: theCase.id, stringType }, transaction, }) - if (comment) { - if (updateComment === null) { - await this.explanatoryCommentModel.destroy({ - where: { caseId: theCase.id, commentType }, + if (caseString) { + if (updateCaseString === null) { + await this.caseStringModel.destroy({ + where: { caseId: theCase.id, stringType }, transaction, }) } else { - await this.explanatoryCommentModel.update( - { comment: updateComment }, - { where: { caseId: theCase.id, commentType }, transaction }, + await this.caseStringModel.update( + { value: updateCaseString }, + { where: { caseId: theCase.id, stringType }, transaction }, ) } - } else if (updateComment !== null) { - await this.explanatoryCommentModel.create( - { caseId: theCase.id, commentType, comment: updateComment }, + } else if (updateCaseString !== null) { + await this.caseStringModel.create( + { caseId: theCase.id, stringType, value: updateCaseString }, { transaction }, ) } - delete update[commentKey] + delete update[caseStringKey] } } } diff --git a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts index cc82fdad76c9..041b896821d5 100644 --- a/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/case/dto/updateCase.dto.ts @@ -513,4 +513,9 @@ export class UpdateCaseDto { @IsUUID() @ApiPropertyOptional({ type: String }) readonly mergeCaseId?: string + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly civilDemands?: string } diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts b/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts index 857ce79b3907..4fdc06391c72 100644 --- a/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts +++ b/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts @@ -52,6 +52,7 @@ const prosecutorFields: (keyof UpdateCaseDto)[] = [ 'requestAppealRulingNotToBePublished', 'indictmentDeniedExplanation', 'indictmentReviewDecision', + 'civilDemands', ] const publicProsecutorFields: (keyof UpdateCaseDto)[] = ['indictmentReviewerId'] diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts new file mode 100644 index 000000000000..c22e624b71a1 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts @@ -0,0 +1,38 @@ +import { map } from 'rxjs/operators' + +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common' + +import { Case } from '../models/case.model' +import { CaseString } from '../models/caseString.model' + +const transformCase = (theCase: Case) => { + return { + ...theCase.toJSON(), + postponedIndefinitelyExplanation: + CaseString.postponedIndefinitelyExplanation(theCase.caseStrings), + civilDemands: CaseString.civilDemands(theCase.caseStrings), + } +} + +@Injectable() +export class CaseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + return next.handle().pipe(map(transformCase)) + } +} + +@Injectable() +export class CasesInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + return next + .handle() + .pipe( + map((cases: Case[]) => cases.map((theCase) => transformCase(theCase))), + ) + } +} diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts deleted file mode 100644 index 7dd3dfdf9e71..000000000000 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' - -import { - CallHandler, - ExecutionContext, - Injectable, - NestInterceptor, -} from '@nestjs/common' - -import { - CaseAppealState, - CaseFileCategory, - isDefenceUser, - isIndictmentCase, - isPrisonStaffUser, - isPrisonSystemUser, - isRestrictionCase, - User, -} from '@island.is/judicial-system/types' - -import { Case } from '../models/case.model' - -@Injectable() -export class CaseFileInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest() - const user: User = request.user - - return next.handle().pipe( - map((data: Case) => { - if (isDefenceUser(user)) { - return data - } - - if ( - isPrisonStaffUser(user) || - (isRestrictionCase(data.type) && - data.appealState !== CaseAppealState.COMPLETED) - ) { - data.caseFiles?.splice(0, data.caseFiles.length) - } else if (isPrisonSystemUser(user)) { - data.caseFiles?.splice( - 0, - data.caseFiles.length, - ...data.caseFiles.filter((cf) => - isIndictmentCase(data.type) - ? cf.category === CaseFileCategory.RULING - : cf.category === CaseFileCategory.APPEAL_RULING, - ), - ) - } - - return data - }), - ) - } -} diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts index 291dda68112e..3044fdff06de 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts @@ -10,8 +10,8 @@ import { import { IndictmentDecision } from '@island.is/judicial-system/types' import { Case } from '../models/case.model' +import { CaseString } from '../models/caseString.model' import { DateLog } from '../models/dateLog.model' -import { ExplanatoryComment } from '../models/explanatoryComment.model' @Injectable() export class CaseListInterceptor implements NestInterceptor { @@ -57,9 +57,7 @@ export class CaseListInterceptor implements NestInterceptor { appealRulingDecision: theCase.appealRulingDecision, prosecutorsOffice: theCase.prosecutorsOffice, postponedIndefinitelyExplanation: - ExplanatoryComment.postponedIndefinitelyExplanation( - theCase.explanatoryComments, - )?.comment, + CaseString.postponedIndefinitelyExplanation(theCase.caseStrings), indictmentReviewer: theCase.indictmentReviewer, indictmentReviewDecision: theCase.indictmentReviewDecision, indictmentDecision: theCase.indictmentDecision, diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseOriginalAncestor.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseOriginalAncestor.interceptor.ts index 35b092269a32..9920b3d6c278 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseOriginalAncestor.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseOriginalAncestor.interceptor.ts @@ -1,5 +1,3 @@ -import { Observable } from 'rxjs' - import { CallHandler, ExecutionContext, @@ -9,18 +7,16 @@ import { } from '@nestjs/common' import { InternalCaseService } from '../internalCase.service' +import { Case } from '../models/case.model' @Injectable() export class CaseOriginalAncestorInterceptor implements NestInterceptor { constructor(private readonly internalCaseService: InternalCaseService) {} - async intercept( - context: ExecutionContext, - next: CallHandler, - ): Promise> { + async intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest() - const theCase = request.case + const theCase: Case = request.case if (!theCase) { throw new InternalServerErrorException('Missing case') diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts index 5ff8d84bff3f..0eef4a4edc04 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts @@ -1,4 +1,3 @@ -import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { @@ -11,29 +10,29 @@ import { import { CaseAppealState, EventType, - InstitutionType, + isDefenceUser, + isPrisonStaffUser, + isProsecutionUser, User, - UserRole, } from '@island.is/judicial-system/types' import { EventLogService } from '../../event-log' -import { Case } from '../models/case.model' @Injectable() export class CompletedAppealAccessedInterceptor implements NestInterceptor { constructor(private readonly eventLogService: EventLogService) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { + intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest() const user: User = request.user return next.handle().pipe( - map((data: Case) => { + map((data) => { if ( data.appealState === CaseAppealState.COMPLETED && - ([UserRole.PROSECUTOR, UserRole.DEFENDER].includes(user.role) || - (user.role === UserRole.PRISON_SYSTEM_STAFF && - user.institution?.type === InstitutionType.PRISON)) + (isProsecutionUser(user) || + isDefenceUser(user) || + isPrisonStaffUser(user)) ) { this.eventLogService.create({ eventType: EventType.APPEAL_RESULT_ACCESSED, diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts new file mode 100644 index 000000000000..70d735d40e37 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts @@ -0,0 +1,36 @@ +import { map } from 'rxjs/operators' + +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common' + +import { CaseFileCategory, User } from '@island.is/judicial-system/types' + +import { canLimitedAcccessUserViewCaseFile } from '../../file' + +@Injectable() +export class LimitedAccessCaseFileInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest() + const user: User = request.user + + return next.handle().pipe( + map((theCase) => { + const caseFiles = theCase.caseFiles?.filter( + ({ category }: { category: CaseFileCategory }) => + canLimitedAcccessUserViewCaseFile( + user, + theCase.type, + theCase.state, + category, + ), + ) + + return { ...theCase, caseFiles } + }), + ) + } +} diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts index 0d73789d2686..c81b939f4a16 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts @@ -6,6 +6,7 @@ import { Param, Post, UseGuards, + UseInterceptors, } from '@nestjs/common' import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger' @@ -32,6 +33,10 @@ import { CurrentCase } from './guards/case.decorator' import { CaseCompletedGuard } from './guards/caseCompleted.guard' import { CaseExistsGuard } from './guards/caseExists.guard' import { CaseTypeGuard } from './guards/caseType.guard' +import { + CaseInterceptor, + CasesInterceptor, +} from './interceptors/case.interceptor' import { ArchiveResponse } from './models/archive.response' import { Case } from './models/case.model' import { DeliverResponse } from './models/deliver.response' @@ -47,6 +52,7 @@ export class InternalCaseController { @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} + @UseInterceptors(CaseInterceptor) @Post('case') @ApiCreatedResponse({ type: Case, description: 'Creates a new case' }) async create(@Body() caseToCreate: InternalCreateCaseDto): Promise { @@ -76,6 +82,7 @@ export class InternalCaseController { isArray: true, description: 'Gets all indictment cases', }) + @UseInterceptors(CasesInterceptor) getIndictmentCases( @Body() internalCasesDto: InternalCasesDto, ): Promise { @@ -86,15 +93,16 @@ export class InternalCaseController { ) } - @Post('cases/indictment/:caseId') + @Post('case/indictment/:caseId') @ApiOkResponse({ type: Case, description: 'Gets indictment case by id', }) + @UseInterceptors(CaseInterceptor) getIndictmentCase( @Param('caseId') caseId: string, @Body() internalCasesDto: InternalCasesDto, - ): Promise { + ): Promise { this.logger.debug(`Getting indictment case ${caseId}`) return this.internalCaseService.getIndictmentCase( diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts index 235d74f860a0..741699620065 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts @@ -64,9 +64,9 @@ import { archiveFilter } from './filters/case.archiveFilter' import { ArchiveResponse } from './models/archive.response' import { Case } from './models/case.model' import { CaseArchive } from './models/caseArchive.model' +import { CaseString } from './models/caseString.model' import { DateLog } from './models/dateLog.model' import { DeliverResponse } from './models/deliver.response' -import { ExplanatoryComment } from './models/explanatoryComment.model' import { caseModuleConfig } from './case.config' import { PdfService } from './pdf.service' @@ -119,9 +119,7 @@ const indictmentCountEncryptionProperties: (keyof IndictmentCount)[] = [ 'legalArguments', ] -const explanatoryCommentEncryptionProperties: (keyof ExplanatoryComment)[] = [ - 'comment', -] +const caseStringEncryptionProperties: (keyof CaseString)[] = ['value'] const collectEncryptionProperties = ( properties: string[], @@ -148,8 +146,8 @@ export class InternalCaseService { constructor( @InjectConnection() private readonly sequelize: Sequelize, - @InjectModel(ExplanatoryComment) - private readonly explanatoryCommentModel: typeof ExplanatoryComment, + @InjectModel(CaseString) + private readonly caseStringModel: typeof CaseString, @InjectModel(Case) private readonly caseModel: typeof Case, @InjectModel(CaseArchive) private readonly caseArchiveModel: typeof CaseArchive, @@ -393,17 +391,13 @@ export class InternalCaseService { { model: Defendant, as: 'defendants' }, { model: IndictmentCount, as: 'indictmentCounts' }, { model: CaseFile, as: 'caseFiles' }, - { model: ExplanatoryComment, as: 'explanatoryComments' }, + { model: CaseString, as: 'caseStrings' }, ], order: [ [{ model: Defendant, as: 'defendants' }, 'created', 'ASC'], [{ model: IndictmentCount, as: 'indictmentCounts' }, 'created', 'ASC'], [{ model: CaseFile, as: 'caseFiles' }, 'created', 'ASC'], - [ - { model: ExplanatoryComment, as: 'explanatoryComments' }, - 'created', - 'ASC', - ], + [{ model: CaseString, as: 'caseStrings' }, 'created', 'ASC'], ], where: archiveFilter, }) @@ -463,19 +457,19 @@ export class InternalCaseService { ) } - const explanatoryCommentsArchive = [] - for (const comment of theCase.explanatoryComments ?? []) { - const [clearedExplanatoryCommentProperties, explanatoryCommentArchive] = + const caseStringsArchive = [] + for (const caseString of theCase.caseStrings ?? []) { + const [clearedCaseStringProperties, caseStringArchive] = collectEncryptionProperties( - explanatoryCommentEncryptionProperties, - comment, + caseStringEncryptionProperties, + caseString, ) - explanatoryCommentsArchive.push(explanatoryCommentArchive) + caseStringsArchive.push(caseStringArchive) - await this.explanatoryCommentModel.update( - clearedExplanatoryCommentProperties, - { where: { id: comment.id, caseId: theCase.id }, transaction }, - ) + await this.caseStringModel.update(clearedCaseStringProperties, { + where: { id: caseString.id, caseId: theCase.id }, + transaction, + }) } await this.caseArchiveModel.create( @@ -487,7 +481,7 @@ export class InternalCaseService { defendants: defendantsArchive, caseFiles: caseFilesArchive, indictmentCounts: indictmentCountsArchive, - explanatoryComments: explanatoryCommentsArchive, + caseStrings: caseStringsArchive, }), this.config.archiveEncryptionKey, { iv: CryptoJS.enc.Hex.parse(uuidFactory()) }, @@ -1234,10 +1228,7 @@ export class InternalCaseService { }) } - async getIndictmentCase( - caseId: string, - nationalId: string, - ): Promise { + async getIndictmentCase(caseId: string, nationalId: string): Promise { // The national id could be without a hyphen or with a hyphen so we need to // search for both const formattedNationalId = formatNationalId(nationalId) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts index 42c90d58ec5e..4663b613d9a8 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts @@ -53,8 +53,9 @@ import { CaseWriteGuard } from './guards/caseWrite.guard' import { LimitedAccessCaseExistsGuard } from './guards/limitedAccessCaseExists.guard' import { RequestSharedWithDefenderGuard } from './guards/requestSharedWithDefender.guard' import { defenderTransitionRule, defenderUpdateRule } from './guards/rolesRules' -import { CaseFileInterceptor } from './interceptors/caseFile.interceptor' +import { CaseInterceptor } from './interceptors/case.interceptor' import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' +import { LimitedAccessCaseFileInterceptor } from './interceptors/limitedAccessCaseFile.interceptor' import { Case } from './models/case.model' import { transitionCase } from './state/case.state' import { @@ -81,12 +82,16 @@ export class LimitedAccessCaseController { CaseReadGuard, ) @RolesRules(prisonSystemStaffRule, defenderRule) + @UseInterceptors( + CompletedAppealAccessedInterceptor, + LimitedAccessCaseFileInterceptor, + CaseInterceptor, + ) @Get('case/:caseId/limitedAccess') @ApiOkResponse({ type: Case, description: 'Gets a limited set of properties of an existing case', }) - @UseInterceptors(CompletedAppealAccessedInterceptor, CaseFileInterceptor) async getById( @Param('caseId') caseId: string, @CurrentCase() theCase: Case, @@ -115,6 +120,7 @@ export class LimitedAccessCaseController { CaseCompletedGuard, ) @RolesRules(defenderUpdateRule) + @UseInterceptors(CaseInterceptor) @Patch('case/:caseId/limitedAccess') @ApiOkResponse({ type: Case, description: 'Updates an existing case' }) update( @@ -143,6 +149,7 @@ export class LimitedAccessCaseController { CaseCompletedGuard, ) @RolesRules(defenderTransitionRule) + @UseInterceptors(CaseInterceptor) @Patch('case/:caseId/limitedAccess/state') @ApiOkResponse({ type: Case, diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index 691c040563a7..0f784551d1f7 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -21,10 +21,10 @@ import { CaseFileCategory, CaseFileState, CaseState, - CommentType, DateType, EventType, NotificationType, + StringType, UserRole, } from '@island.is/judicial-system/types' @@ -39,8 +39,8 @@ import { import { Institution } from '../institution' import { User } from '../user' import { Case } from './models/case.model' +import { CaseString } from './models/caseString.model' import { DateLog } from './models/dateLog.model' -import { ExplanatoryComment } from './models/explanatoryComment.model' import { PdfService } from './pdf.service' export const attributes: (keyof Case)[] = [ @@ -116,7 +116,7 @@ export interface LimitedAccessUpdateCase const eventTypes = Object.values(EventType) const dateTypes = Object.values(DateType) -const commentTypes = Object.values(CommentType) +const stringTypes = Object.values(StringType) export const include: Includeable[] = [ { model: Institution, as: 'prosecutorsOffice' }, @@ -191,7 +191,6 @@ export const include: Includeable[] = [ CaseFileCategory.CRIMINAL_RECORD, CaseFileCategory.COST_BREAKDOWN, CaseFileCategory.CASE_FILE, - CaseFileCategory.CASE_FILE_RECORD, CaseFileCategory.PROSECUTOR_CASE_FILE, CaseFileCategory.DEFENDANT_CASE_FILE, ], @@ -212,10 +211,10 @@ export const include: Includeable[] = [ where: { dateType: { [Op.in]: dateTypes } }, }, { - model: ExplanatoryComment, - as: 'explanatoryComments', + model: CaseString, + as: 'caseStrings', required: false, - where: { commentType: { [Op.in]: commentTypes } }, + where: { stringType: { [Op.in]: stringTypes } }, }, ] diff --git a/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts b/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts index 96453bfa9e01..bd26c92f2267 100644 --- a/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts +++ b/apps/judicial-system/backend/src/app/modules/case/models/case.model.ts @@ -44,8 +44,8 @@ import { IndictmentCount } from '../../indictment-count' import { Institution } from '../../institution' import { Notification } from '../../notification' import { User } from '../../user' +import { CaseString } from './caseString.model' import { DateLog } from './dateLog.model' -import { ExplanatoryComment } from './explanatoryComment.model' @Table({ tableName: 'case', @@ -901,11 +901,11 @@ export class Case extends Model { dateLogs?: DateLog[] /********** - * The case's explanatory comments + * The case's strings **********/ - @HasMany(() => ExplanatoryComment, 'caseId') - @ApiPropertyOptional({ type: ExplanatoryComment, isArray: true }) - explanatoryComments?: ExplanatoryComment[] + @HasMany(() => CaseString, 'caseId') + @ApiPropertyOptional({ type: CaseString, isArray: true }) + caseStrings?: CaseString[] /********** * The appeal ruling expiration date and time - example: the end of custody in custody cases - diff --git a/apps/judicial-system/backend/src/app/modules/case/models/explanatoryComment.model.ts b/apps/judicial-system/backend/src/app/modules/case/models/caseString.model.ts similarity index 53% rename from apps/judicial-system/backend/src/app/modules/case/models/explanatoryComment.model.ts rename to apps/judicial-system/backend/src/app/modules/case/models/caseString.model.ts index 45a6b0cc3b79..38c4b4ac37af 100644 --- a/apps/judicial-system/backend/src/app/modules/case/models/explanatoryComment.model.ts +++ b/apps/judicial-system/backend/src/app/modules/case/models/caseString.model.ts @@ -10,23 +10,26 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' -import { CommentType } from '@island.is/judicial-system/types' +import { StringType } from '@island.is/judicial-system/types' import { Case } from './case.model' @Table({ - tableName: 'explanatory_comment', + tableName: 'case_string', timestamps: true, }) -export class ExplanatoryComment extends Model { - static postponedIndefinitelyExplanation( - explanatoryComments?: ExplanatoryComment[], - ) { - return explanatoryComments?.find( - (explanatoryComment) => - explanatoryComment.commentType === - CommentType.POSTPONED_INDEFINITELY_EXPLANATION, - ) +export class CaseString extends Model { + static postponedIndefinitelyExplanation(caseStrings?: CaseString[]) { + return caseStrings?.find( + (caseString) => + caseString.stringType === StringType.POSTPONED_INDEFINITELY_EXPLANATION, + )?.value + } + + static civilDemands(caseStrings?: CaseString[]) { + return caseStrings?.find( + (caseString) => caseString.stringType === StringType.CIVIL_DEMANDS, + )?.value } @Column({ @@ -49,17 +52,17 @@ export class ExplanatoryComment extends Model { @Column({ type: DataType.ENUM, allowNull: false, - values: Object.values(CommentType), + values: Object.values(StringType), }) - @ApiProperty({ enum: CommentType }) - commentType!: CommentType + @ApiProperty({ enum: StringType }) + stringType!: StringType @ForeignKey(() => Case) @Column({ type: DataType.UUID, allowNull: false }) @ApiPropertyOptional({ type: String }) caseId!: string - @Column({ type: DataType.STRING, allowNull: false }) + @Column({ type: DataType.TEXT, allowNull: false }) @ApiPropertyOptional({ type: String }) - comment!: string + value!: string } diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts index c282283ddfad..0ffde6ad16ca 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts @@ -9,13 +9,13 @@ import { CaseOrigin, CaseState, CaseType, - CommentType, DateType, indictmentCases, InstitutionType, investigationCases, NotificationType, restrictionCases, + StringType, User, UserRole, } from '@island.is/judicial-system/types' @@ -28,8 +28,8 @@ import { FileService } from '../../../file' import { UserService } from '../../../user' import { UpdateCaseDto } from '../../dto/updateCase.dto' import { Case } from '../../models/case.model' +import { CaseString } from '../../models/caseString.model' import { DateLog } from '../../models/dateLog.model' -import { ExplanatoryComment } from '../../models/explanatoryComment.model' jest.mock('../../../../factories') @@ -73,7 +73,7 @@ describe('CaseController - Update', () => { let transaction: Transaction let mockCaseModel: typeof Case let mockDateLogModel: typeof DateLog - let mockExplanatoryCommentModel: typeof ExplanatoryComment + let mockCaseStringModel: typeof CaseString let givenWhenThen: GivenWhenThen beforeEach(async () => { @@ -84,7 +84,7 @@ describe('CaseController - Update', () => { sequelize, caseModel, dateLogModel, - explanatoryCommentModel, + caseStringModel, caseController, } = await createTestingCaseModule() @@ -93,7 +93,7 @@ describe('CaseController - Update', () => { mockFileService = fileService mockCaseModel = caseModel mockDateLogModel = dateLogModel - mockExplanatoryCommentModel = explanatoryCommentModel + mockCaseStringModel = caseStringModel const mockTransaction = sequelize.transaction as jest.Mock transaction = {} as Transaction @@ -913,11 +913,31 @@ describe('CaseController - Update', () => { }) it('should update case', () => { - expect(mockExplanatoryCommentModel.create).toHaveBeenCalledWith( + expect(mockCaseStringModel.create).toHaveBeenCalledWith( { - commentType: CommentType.POSTPONED_INDEFINITELY_EXPLANATION, + stringType: StringType.POSTPONED_INDEFINITELY_EXPLANATION, caseId, - comment: postponedIndefinitelyExplanation, + value: postponedIndefinitelyExplanation, + }, + { transaction }, + ) + }) + }) + + describe('civil demands updated', () => { + const civilDemands = uuid() + const caseToUpdate = { civilDemands } + + beforeEach(async () => { + await givenWhenThen(caseId, user, theCase, caseToUpdate) + }) + + it('should update case', () => { + expect(mockCaseStringModel.create).toHaveBeenCalledWith( + { + stringType: StringType.CIVIL_DEMANDS, + caseId, + value: civilDemands, }, { transaction }, ) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts b/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts index 77035ca9a36c..1e8dd996ea45 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts @@ -33,8 +33,8 @@ import { LimitedAccessCaseController } from '../limitedAccessCase.controller' import { LimitedAccessCaseService } from '../limitedAccessCase.service' import { Case } from '../models/case.model' import { CaseArchive } from '../models/caseArchive.model' +import { CaseString } from '../models/caseString.model' import { DateLog } from '../models/dateLog.model' -import { ExplanatoryComment } from '../models/explanatoryComment.model' import { PdfService } from '../pdf.service' jest.mock('@island.is/judicial-system/message') @@ -114,7 +114,7 @@ export const createTestingCaseModule = async () => { }, }, { - provide: getModelToken(ExplanatoryComment), + provide: getModelToken(CaseString), useValue: { create: jest.fn(), findOne: jest.fn(), @@ -164,8 +164,8 @@ export const createTestingCaseModule = async () => { const dateLogModel = caseModule.get(getModelToken(DateLog)) - const explanatoryCommentModel = caseModule.get( - getModelToken(ExplanatoryComment), + const caseStringModel = caseModule.get( + getModelToken(CaseString), ) const caseConfig = caseModule.get>( @@ -203,7 +203,7 @@ export const createTestingCaseModule = async () => { caseModel, caseArchiveModel, dateLogModel, - explanatoryCommentModel, + caseStringModel, caseConfig, caseService, limitedAccessCaseService, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts index 5db947012958..d6beb070a348 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/archive.spec.ts @@ -20,7 +20,7 @@ import { archiveFilter } from '../../filters/case.archiveFilter' import { ArchiveResponse } from '../../models/archive.response' import { Case } from '../../models/case.model' import { CaseArchive } from '../../models/caseArchive.model' -import { ExplanatoryComment } from '../../models/explanatoryComment.model' +import { CaseString } from '../../models/caseString.model' jest.mock('crypto-js') jest.mock('../../../../factories') @@ -36,7 +36,7 @@ describe('InternalCaseController - Archive', () => { let mockFileService: FileService let mockDefendantService: DefendantService let mockIndictmentCountService: IndictmentCountService - let mockExplanatoryCommentModel: typeof ExplanatoryComment + let mockCaseStringModel: typeof CaseString let mockCaseModel: typeof Case let mockCaseArchiveModel: typeof CaseArchive let mockCaseConfig: ConfigType @@ -49,7 +49,7 @@ describe('InternalCaseController - Archive', () => { defendantService, indictmentCountService, sequelize, - explanatoryCommentModel, + caseStringModel, caseModel, caseArchiveModel, caseConfig, @@ -59,7 +59,7 @@ describe('InternalCaseController - Archive', () => { mockFileService = fileService mockDefendantService = defendantService mockIndictmentCountService = indictmentCountService - mockExplanatoryCommentModel = explanatoryCommentModel + mockCaseStringModel = caseStringModel mockCaseModel = caseModel mockCaseArchiveModel = caseArchiveModel mockCaseConfig = caseConfig @@ -90,8 +90,8 @@ describe('InternalCaseController - Archive', () => { const caseFileId2 = uuid() const indictmentCountId1 = uuid() const indictmentCountId2 = uuid() - const explanatoryCommentId1 = uuid() - const explanatoryCommentId2 = uuid() + const caseStringId1 = uuid() + const caseStringId2 = uuid() const theCase = { id: caseId, description: 'original_description', @@ -168,9 +168,9 @@ describe('InternalCaseController - Archive', () => { indictmentDeniedExplanation: 'original_indictment_denied_explanation', indictmentReturnedExplanation: 'original_indictment_returned_explanation', isArchived: false, - explanatoryComments: [ - { id: explanatoryCommentId1, comment: 'original_comment1' }, - { id: explanatoryCommentId2, comment: 'original_comment2' }, + caseStrings: [ + { id: caseStringId1, value: 'original_comment1' }, + { id: caseStringId2, value: 'original_comment2' }, ], } const archive = JSON.stringify({ @@ -240,9 +240,9 @@ describe('InternalCaseController - Archive', () => { legalArguments: 'original_legal_arguments2', }, ], - explanatoryComments: [ - { comment: 'original_comment1' }, - { comment: 'original_comment2' }, + caseStrings: [ + { value: 'original_comment1' }, + { value: 'original_comment2' }, ], }) const iv = uuid() @@ -251,9 +251,8 @@ describe('InternalCaseController - Archive', () => { let then: Then beforeEach(async () => { - const mockUpdateExplanatoryComment = - mockExplanatoryCommentModel.update as jest.Mock - mockUpdateExplanatoryComment.mockResolvedValueOnce([1]) + const mockUpdateCaseString = mockCaseStringModel.update as jest.Mock + mockUpdateCaseString.mockResolvedValueOnce([1]) const mockFindOne = mockCaseModel.findOne as jest.Mock mockFindOne.mockResolvedValueOnce(theCase) const mockUpdate = mockCaseModel.update as jest.Mock @@ -274,7 +273,7 @@ describe('InternalCaseController - Archive', () => { { model: Defendant, as: 'defendants' }, { model: IndictmentCount, as: 'indictmentCounts' }, { model: CaseFile, as: 'caseFiles' }, - { model: ExplanatoryComment, as: 'explanatoryComments' }, + { model: CaseString, as: 'caseStrings' }, ], order: [ [{ model: Defendant, as: 'defendants' }, 'created', 'ASC'], @@ -284,11 +283,7 @@ describe('InternalCaseController - Archive', () => { 'ASC', ], [{ model: CaseFile, as: 'caseFiles' }, 'created', 'ASC'], - [ - { model: ExplanatoryComment, as: 'explanatoryComments' }, - 'created', - 'ASC', - ], + [{ model: CaseString, as: 'caseStrings' }, 'created', 'ASC'], ], where: archiveFilter, }) @@ -336,13 +331,13 @@ describe('InternalCaseController - Archive', () => { }, transaction, ) - expect(mockExplanatoryCommentModel.update).toHaveBeenCalledWith( - { comment: '' }, - { where: { id: explanatoryCommentId1, caseId }, transaction }, + expect(mockCaseStringModel.update).toHaveBeenCalledWith( + { value: '' }, + { where: { id: caseStringId1, caseId }, transaction }, ) - expect(mockExplanatoryCommentModel.update).toHaveBeenCalledWith( - { comment: '' }, - { where: { id: explanatoryCommentId2, caseId }, transaction }, + expect(mockCaseStringModel.update).toHaveBeenCalledWith( + { value: '' }, + { where: { id: caseStringId2, caseId }, transaction }, ) expect(CryptoJS.enc.Hex.parse).toHaveBeenCalledWith(iv) expect(CryptoJS.AES.encrypt).toHaveBeenCalledWith( diff --git a/apps/judicial-system/backend/src/app/modules/event/event.service.ts b/apps/judicial-system/backend/src/app/modules/event/event.service.ts index e66825a614af..aeac0ff7f26d 100644 --- a/apps/judicial-system/backend/src/app/modules/event/event.service.ts +++ b/apps/judicial-system/backend/src/app/modules/event/event.service.ts @@ -18,8 +18,8 @@ import { } from '@island.is/judicial-system/types' import { type Case } from '../case' +import { CaseString } from '../case/models/caseString.model' import { DateLog } from '../case/models/dateLog.model' -import { ExplanatoryComment } from '../case/models/explanatoryComment.model' import { eventModuleConfig } from './event.config' const errorEmojis = [ @@ -121,15 +121,11 @@ export class EventService { }\n>Dómritari ${ theCase.registrar?.name ?? 'er ekki skráður' }\n>Fyrirtaka ${ - ExplanatoryComment.postponedIndefinitelyExplanation( - theCase.explanatoryComments, - ) - ? 'ekki ákveðin' - : formatDate( - DateLog.courtDate(theCase.dateLogs)?.date ?? - DateLog.arraignmentDate(theCase.dateLogs)?.date, - 'Pp', - ) ?? 'er ekki skráð' + formatDate( + DateLog.courtDate(theCase.dateLogs)?.date ?? + DateLog.arraignmentDate(theCase.dateLogs)?.date, + 'Pp', + ) ?? 'er ekki skráð' }` : '' diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts index 60f536547067..020af77d5a4f 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts @@ -1,4 +1,14 @@ -import { CaseFileCategory } from '@island.is/judicial-system/types' +import { + CaseFileCategory, + CaseState, + CaseType, + isCompletedCase, + isDefenceUser, + isIndictmentCase, + isPrisonAdminUser, + isRequestCase, + User, +} from '@island.is/judicial-system/types' export const defenderCaseFileCategoriesForRestrictionAndInvestigationCases = [ CaseFileCategory.PROSECUTOR_APPEAL_BRIEF, @@ -12,7 +22,7 @@ export const defenderCaseFileCategoriesForRestrictionAndInvestigationCases = [ CaseFileCategory.APPEAL_COURT_RECORD, ] -export const defenderCaseFileCategoriesForIndictmentCases = [ +const defenderCaseFileCategoriesForIndictmentCases = [ CaseFileCategory.COURT_RECORD, CaseFileCategory.RULING, CaseFileCategory.INDICTMENT, @@ -23,7 +33,47 @@ export const defenderCaseFileCategoriesForIndictmentCases = [ CaseFileCategory.DEFENDANT_CASE_FILE, ] -export const prisonAdminCaseFileCategories = [ +const prisonAdminCaseFileCategories = [ CaseFileCategory.APPEAL_RULING, CaseFileCategory.RULING, ] + +export const canLimitedAcccessUserViewCaseFile = ( + user: User, + caseType: CaseType, + caseState: CaseState, + caseFileCategory?: CaseFileCategory, +) => { + if (!caseFileCategory) { + return false + } + + if (isDefenceUser(user)) { + if ( + isRequestCase(caseType) && + isCompletedCase(caseState) && + defenderCaseFileCategoriesForRestrictionAndInvestigationCases.includes( + caseFileCategory, + ) + ) { + return true + } + + if ( + isIndictmentCase(caseType) && + defenderCaseFileCategoriesForIndictmentCases.includes(caseFileCategory) + ) { + return true + } + } + + if ( + isPrisonAdminUser(user) && + isCompletedCase(caseState) && + prisonAdminCaseFileCategories.includes(caseFileCategory) + ) { + return true + } + + return false +} diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts index a8c2f8295ea7..b9bdf659e23b 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts @@ -6,22 +6,11 @@ import { InternalServerErrorException, } from '@nestjs/common' -import { - isCompletedCase, - isDefenceUser, - isIndictmentCase, - isPrisonAdminUser, - isRequestCase, - User, -} from '@island.is/judicial-system/types' +import { User } from '@island.is/judicial-system/types' import { Case } from '../../case' import { CaseFile } from '../models/file.model' -import { - defenderCaseFileCategoriesForIndictmentCases, - defenderCaseFileCategoriesForRestrictionAndInvestigationCases, - prisonAdminCaseFileCategories, -} from './caseFileCategory' +import { canLimitedAcccessUserViewCaseFile } from './caseFileCategory' @Injectable() export class LimitedAccessViewCaseFileGuard implements CanActivate { @@ -46,30 +35,13 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { throw new InternalServerErrorException('Missing case file') } - if (isDefenceUser(user) && caseFile.category) { - if ( - isRequestCase(theCase.type) && - isCompletedCase(theCase.state) && - defenderCaseFileCategoriesForRestrictionAndInvestigationCases.includes( - caseFile.category, - ) - ) { - return true - } - - if ( - isIndictmentCase(theCase.type) && - defenderCaseFileCategoriesForIndictmentCases.includes(caseFile.category) - ) { - return true - } - } - if ( - caseFile.category && - isCompletedCase(theCase.state) && - isPrisonAdminUser(user) && - prisonAdminCaseFileCategories.includes(caseFile.category) + canLimitedAcccessUserViewCaseFile( + user, + theCase.type, + theCase.state, + caseFile.category, + ) ) { return true } diff --git a/apps/judicial-system/backend/src/app/modules/file/index.ts b/apps/judicial-system/backend/src/app/modules/file/index.ts index 9b52be8a344c..34a68700f3fc 100644 --- a/apps/judicial-system/backend/src/app/modules/file/index.ts +++ b/apps/judicial-system/backend/src/app/modules/file/index.ts @@ -1,3 +1,6 @@ export { CaseFile } from './models/file.model' export { FileService } from './file.service' -export { defenderCaseFileCategoriesForRestrictionAndInvestigationCases } from './guards/caseFileCategory' +export { + canLimitedAcccessUserViewCaseFile, + defenderCaseFileCategoriesForRestrictionAndInvestigationCases, +} from './guards/caseFileCategory' diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts index 660bb3e5ff7b..919a6f7e4fed 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/case.service.ts @@ -190,7 +190,7 @@ export class CaseService { ): Promise { try { const res = await fetch( - `${this.config.backendUrl}/api/internal/cases/indictment/${id}`, + `${this.config.backendUrl}/api/internal/case/indictment/${id}`, { method: 'POST', headers: { diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index 96bd2420103f..1c9635d77d2c 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -308,5 +308,6 @@ query Case($input: CaseQueryInput!) { policeCaseNumbers indictmentSubtypes } + civilDemands } } diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.strings.ts b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.strings.ts index 6e9583784fda..2eb35ef19d13 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.strings.ts +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.strings.ts @@ -94,4 +94,22 @@ export const indictment = defineMessages({ defaultMessage: 'Ákæra - PDF', description: 'Notaður sem texti á hnappi til að sækja ákæru sem PDF skjal.', }, + civilDemandsTitle: { + id: 'judicial.system.core:indictments_indictment.civil_demands_title', + defaultMessage: 'Einkaréttarkrafa', + description: + 'Notaður sem titill á Einkaréttarkrafa svæði á ákæra skrefi í ákærum.', + }, + civilDemandsLabel: { + id: 'judicial.system.core:indictments_indictment.civil_demands_label', + defaultMessage: 'Einkaréttarkrafa', + description: + 'Notaður sem titill á Einkaréttarkrafa textasvæði á ákæra skrefi í ákærum.', + }, + civilDemandsPlaceholder: { + id: 'judicial.system.core:indictments_indictment.civil_demands_placeholder', + defaultMessage: 'Hver er krafa kröfuhafa?', + description: + 'Notaður sem skýritexti á Einkaréttarkrafa textasvæði á ákæra skrefi í ákærum.', + }, }) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.tsx index a2cc60c4ff1c..b0b95552c0e3 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Indictment.tsx @@ -97,7 +97,8 @@ const Indictment = () => { indictmentIntroductionErrorMessage, setIndictmentIntroductionErrorMessage, ] = useState('') - const [demandsErrorMessage, setDemandsErrorMessage] = useState('') + const [demandsErrorMessage, setDemandsErrorMessage] = useState('') + const [civilDemandsErrorMessage, setCivilDemandsErrorMessage] = useState('') const { data: policeCaseData } = usePoliceCaseInfoQuery({ variables: { @@ -426,7 +427,7 @@ const Indictment = () => { name="demands" label={formatMessage(strings.demandsLabel)} placeholder={formatMessage(strings.demandsPlaceholder)} - value={workingCase.demands || ''} + value={workingCase.demands ?? ''} errorMessage={demandsErrorMessage} hasError={demandsErrorMessage !== ''} onChange={(event) => @@ -457,6 +458,44 @@ const Indictment = () => { /> + + + + + removeTabsValidateAndSet( + 'civilDemands', + event.target.value, + ['empty'], + setWorkingCase, + civilDemandsErrorMessage, + setCivilDemandsErrorMessage, + ) + } + onBlur={(event) => + validateAndSendToServer( + 'civilDemands', + event.target.value, + ['empty'], + workingCase, + updateCase, + setCivilDemandsErrorMessage, + ) + } + textarea + autoComplete="off" + required + rows={7} + autoExpand={{ on: true, maxHeight: 300 }} + /> + + { - return Boolean(workingCase.demands) + return Boolean(workingCase.demands && workingCase.civilDemands) } export const isPoliceDemandsStepValidRC = (workingCase: Case): boolean => { diff --git a/libs/judicial-system/types/src/index.ts b/libs/judicial-system/types/src/index.ts index 772c1463302e..9694c67c1755 100644 --- a/libs/judicial-system/types/src/index.ts +++ b/libs/judicial-system/types/src/index.ts @@ -12,7 +12,7 @@ export { NotificationType } from './lib/notification' export type { Institution } from './lib/institution' export { EventType } from './lib/eventLog' export { DateType } from './lib/dateLog' -export { CommentType } from './lib/comment' +export { StringType } from './lib/caseString' export { CaseFileState, CaseFileCategory } from './lib/file' diff --git a/libs/judicial-system/types/src/lib/comment.ts b/libs/judicial-system/types/src/lib/caseString.ts similarity index 56% rename from libs/judicial-system/types/src/lib/comment.ts rename to libs/judicial-system/types/src/lib/caseString.ts index 2b8f1cbaf20b..2f78b27d1577 100644 --- a/libs/judicial-system/types/src/lib/comment.ts +++ b/libs/judicial-system/types/src/lib/caseString.ts @@ -1,3 +1,4 @@ -export enum CommentType { +export enum StringType { POSTPONED_INDEFINITELY_EXPLANATION = 'POSTPONED_INDEFINITELY_EXPLANATION', + CIVIL_DEMANDS = 'CIVIL_DEMANDS', } From 7de9b1383bf454d16e42b3de9ddedbcd520375d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigr=C3=BAn=20Tinna=20Gissurard=C3=B3ttir?= <39527334+sigruntg@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:32:45 +0000 Subject: [PATCH 043/173] fix(register-new-machine): small fixes (#15993) * multiple fixes for register new machine * add maxLength to text fields * adding custom changeanswers --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../register-new-machine.service.ts | 24 ++-- .../src/fields/AboutMachine/index.tsx | 2 + .../src/fields/ChangeAnswers/index.tsx | 34 ++++++ .../src/fields/LicensePlates/index.tsx | 60 ---------- .../src/fields/MachineType/index.tsx | 16 ++- .../src/fields/Overview/index.tsx | 12 +- .../register-new-machine/src/fields/index.ts | 2 +- .../InformationSection/ImporterInformation.ts | 108 ++++++------------ .../InformationSection/OperatorInformation.ts | 15 ++- .../InformationSection/OwnerInformation.ts | 107 +++++++++++++++++ .../InformationSection/index.ts | 7 +- .../MachineSection/MachineBasicInformation.ts | 13 ++- .../MachineSection/MachineLicensePlate.ts | 12 +- .../src/lib/dataSchema.ts | 10 +- .../src/lib/messages/information.ts | 32 ++++-- .../src/lib/messages/overview.ts | 2 +- .../src/utils/canMaybeRegisterToTraffic.ts | 2 +- .../src/utils/canRegisterToTraffic.ts | 15 ++- .../src/utils/getTechnicalInformation.ts | 18 ++- .../src/utils/isOwnerOtherThanImporter.ts | 2 +- .../work-machines/src/clientConfig.json | 10 ++ libs/shared/utils/src/index.ts | 1 + libs/shared/utils/src/lib/postalCodes.ts | 17 +++ 23 files changed, 332 insertions(+), 189 deletions(-) create mode 100644 libs/application/templates/aosh/register-new-machine/src/fields/ChangeAnswers/index.tsx delete mode 100644 libs/application/templates/aosh/register-new-machine/src/fields/LicensePlates/index.tsx create mode 100644 libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OwnerInformation.ts create mode 100644 libs/shared/utils/src/lib/postalCodes.ts diff --git a/libs/application/template-api-modules/src/lib/modules/templates/aosh/register-new-machine/register-new-machine.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/aosh/register-new-machine/register-new-machine.service.ts index 6750c34a3f4a..27ed649dd0b7 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/aosh/register-new-machine/register-new-machine.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/aosh/register-new-machine/register-new-machine.service.ts @@ -61,15 +61,17 @@ export class RegisterNewMachineTemplateService extends BaseTemplateApiService { auth, }: TemplateApiModuleActionProps): Promise { const answers = application.answers as unknown as NewMachineAnswers - const techInfo = {} as { [key: string]: string | undefined } + const techInfo = {} as { [key: string]: string | boolean | undefined } answers.techInfo.forEach(({ variableName, value }) => { if (variableName && value) { - techInfo[variableName] = value + techInfo[variableName] = + value === 'yes' ? true : value === 'no' ? false : value } }) await this.workMachineClientService.addNewMachine(auth, { + xCorrelationID: application.id, machineRegistrationCreateDto: { importer: { nationalId: answers.importerInformation.importer.nationalId, @@ -83,20 +85,20 @@ export class RegisterNewMachineTemplateService extends BaseTemplateApiService { email: answers.importerInformation.importer.email, }, owner: - answers.importerInformation.isOwnerOtherThanImporter === 'yes' + answers.ownerInformation.isOwnerOtherThanImporter === 'yes' ? { - nationalId: answers.importerInformation.owner?.nationalId ?? '', - name: answers.importerInformation.owner?.name ?? '', - address: answers.importerInformation.owner?.address ?? '', - postalCode: answers.importerInformation.owner?.postCode - ? parseInt(answers.importerInformation.owner?.postCode, 10) + nationalId: answers.ownerInformation.owner?.nationalId ?? '', + name: answers.ownerInformation.owner?.name ?? '', + address: answers.ownerInformation.owner?.address ?? '', + postalCode: answers.ownerInformation.owner?.postCode + ? parseInt(answers.ownerInformation.owner?.postCode, 10) : 0, - gsm: answers.importerInformation.owner?.phone ?? '', - email: answers.importerInformation.owner?.email ?? '', + gsm: answers.ownerInformation.owner?.phone ?? '', + email: answers.ownerInformation.owner?.email ?? '', } : undefined, supervisor: - answers.operatorInformation.hasOperator === 'yes' + answers.operatorInformation?.hasOperator === 'yes' ? { nationalId: answers.operatorInformation.operator?.nationalId ?? '', diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx index 701a8b9343fb..6eee5336c886 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx @@ -130,6 +130,7 @@ export const AboutMachine: FC> = ( backgroundColor="blue" required disabled={fromService} + maxLength={50} onChange={(e) => setType(e.target.value)} error={ displayError && type.length === 0 @@ -145,6 +146,7 @@ export const AboutMachine: FC> = ( backgroundColor="blue" required disabled={fromService} + maxLength={50} onChange={(e) => setModel(e.target.value)} error={ displayError && model.length === 0 diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/ChangeAnswers/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/ChangeAnswers/index.tsx new file mode 100644 index 000000000000..2bcdb329d8f9 --- /dev/null +++ b/libs/application/templates/aosh/register-new-machine/src/fields/ChangeAnswers/index.tsx @@ -0,0 +1,34 @@ +import { FieldBaseProps, NO } from '@island.is/application/types' +import { FC } from 'react' +import { useFormContext } from 'react-hook-form' +import { getValueViaPath } from '@island.is/application/core' +import { PersonInformation } from '../../lib/dataSchema' + +interface ChangeAnswersProps { + field: { + props: { + sectionName: string + questionName: string + person: string + } + } +} + +export const ChangeAnswers: FC< + React.PropsWithChildren +> = (props) => { + const { application, field } = props + const { sectionName, questionName, person } = field.props + const { setValue } = useFormContext() + + const personInformation = getValueViaPath( + application.answers, + `${sectionName}.${person}`, + ) as PersonInformation + + if (!personInformation) { + setValue(`${sectionName}.${questionName}`, NO) + } + + return <> +} diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/LicensePlates/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/LicensePlates/index.tsx deleted file mode 100644 index b1506d16f7c5..000000000000 --- a/libs/application/templates/aosh/register-new-machine/src/fields/LicensePlates/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FieldBaseProps } from '@island.is/application/types' -import { - Box, - GridColumn, - GridRow, - RadioButton, -} from '@island.is/island-ui/core' -import { FC } from 'react' -import { useLocale } from '@island.is/localization' -import { Controller } from 'react-hook-form' -import { Plate } from '../../shared/types' -import { licensePlate } from '../../lib/messages' -import { plate110 } from '../../assets/plates/plate-110-510' -import { plate200 } from '../../assets/plates/plate-200-280' -import { plate155 } from '../../assets/plates/plate-155-305' - -export const LicensePlates: FC> = ( - props, -) => { - const { application, field } = props - const { formatMessage } = useLocale() - - const options = [ - { - value: Plate.A, - label: licensePlate.labels.plate110, - illustration: plate110, - }, - { - value: Plate.B, - label: licensePlate.labels.plate200, - illustration: plate200, - }, - { - value: Plate.D, - label: licensePlate.labels.plate155, - illustration: plate155, - }, - ] - - return ( - - {/* Setja radio controller með engum border - 3 saman */} - ( - - {options.map((option, index) => ( - - ))} - - )} - /> - - ) -} diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx index 79ebbf51bc0e..63d02c50394f 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx @@ -116,10 +116,18 @@ export const MachineType: FC> = ( setBeforeSubmitCallback?.(async () => { // Call updateApplication for basicInformation.type and basicInformation.model // Get information for basicInformation here and updateApplication - const response = - type && model && type !== 'unknown' && model !== 'unknown' - ? await getMachineCategoryCallback(type, model) - : undefined + if (modelFromAnswers === model && typeFromAnswers === type) { + return [true, null] + } + let response = undefined + try { + response = + type && model && type !== 'unknown' && model !== 'unknown' + ? await getMachineCategoryCallback(type, model) + : undefined + } catch (e) { + console.error('Could not get machine category', e) + } const category = response?.getMachineParentCategoryByTypeAndModel?.name ?? '' diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/Overview/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/Overview/index.tsx index e0b8b07493c6..515a21d290d3 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/Overview/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/Overview/index.tsx @@ -67,7 +67,7 @@ export const Overview: FC> = ({ label: information.labels.owner.title, value: isOwnerOtherThanImporter(application.answers) ? getPersonInformationForOverview( - 'importerInformation.owner', + 'ownerInformation.owner', application.answers, formatMessage, ) @@ -133,7 +133,7 @@ export const Overview: FC> = ({ component: FieldComponents.KEY_VALUE, title: '', label: machine.labels.technicalMachineInformation.overviewTitle, - value: getTechnicalInformation(application.answers), + value: getTechnicalInformation(application.answers, formatMessage), }} /> @@ -160,14 +160,6 @@ export const Overview: FC> = ({ /> )} - - {canMaybeRegisterToTraffic(application.answers) && ( - - )} ) } diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/index.ts b/libs/application/templates/aosh/register-new-machine/src/fields/index.ts index 1dd52b1b77bc..cf08b813c38b 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/index.ts +++ b/libs/application/templates/aosh/register-new-machine/src/fields/index.ts @@ -1,5 +1,5 @@ export { MachineType } from './MachineType' -export { LicensePlates } from './LicensePlates' export { Overview } from './Overview' export { AboutMachine } from './AboutMachine' export { TechnicalInfo } from './TechnicalInfo' +export { ChangeAnswers } from './ChangeAnswers' diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/ImporterInformation.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/ImporterInformation.ts index 8a5b1ef7ddc7..763455a3db20 100644 --- a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/ImporterInformation.ts +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/ImporterInformation.ts @@ -3,14 +3,13 @@ import { buildTextField, buildSubSection, buildPhoneField, - buildRadioField, - buildDescriptionField, getValueViaPath, + buildSelectField, + buildCustomField, } from '@island.is/application/core' import { information } from '../../../lib/messages' import { Application } from '@island.is/api/schema' -import { FormValue, NO, YES } from '@island.is/application/types' -import { isOwnerOtherThanImporter } from '../../../utils/isOwnerOtherThanImporter' +import { postalCodes } from '@island.is/shared/utils' export const ImporterInformationSubSection = buildSubSection({ id: 'importerInformation', @@ -27,6 +26,7 @@ export const ImporterInformationSubSection = buildSubSection({ backgroundColor: 'white', width: 'half', readOnly: true, + maxLength: 100, defaultValue: (application: Application) => getValueViaPath( application.externalData, @@ -53,6 +53,7 @@ export const ImporterInformationSubSection = buildSubSection({ title: information.labels.importer.address, width: 'half', required: true, + maxLength: 50, defaultValue: (application: Application) => getValueViaPath( application.externalData, @@ -60,12 +61,16 @@ export const ImporterInformationSubSection = buildSubSection({ '', ) as string, }), - buildTextField({ + buildSelectField({ id: 'importerInformation.importer.postCode', title: information.labels.importer.postCode, width: 'half', required: true, - variant: 'number', + options: () => { + return postalCodes.map((code) => { + return { value: `${code}`, label: `${code}` } + }) + }, defaultValue: (application: Application) => getValueViaPath( application.externalData, @@ -90,6 +95,7 @@ export const ImporterInformationSubSection = buildSubSection({ title: information.labels.importer.email, width: 'half', variant: 'email', + maxLength: 250, required: true, defaultValue: (application: Application) => getValueViaPath( @@ -98,72 +104,30 @@ export const ImporterInformationSubSection = buildSubSection({ '', ) as string, }), - buildDescriptionField({ - id: 'importerInformation.description', - title: information.labels.importer.isOwnerOtherThenImporter, - marginTop: 4, - titleVariant: 'h5', - }), - buildRadioField({ - id: 'importerInformation.isOwnerOtherThanImporter', - title: '', - width: 'half', - defaultValue: NO, - options: [ - { - value: NO, - label: information.labels.radioButtons.radioOptionNo, - }, - { - value: YES, - label: information.labels.radioButtons.radioOptionYes, - }, - ], - }), - buildTextField({ - id: 'importerInformation.owner.name', - title: information.labels.owner.name, - width: 'half', - required: true, - condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), - }), - buildTextField({ - id: 'importerInformation.owner.nationalId', - title: information.labels.owner.nationalId, - width: 'half', - required: true, - condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), - }), - buildTextField({ - id: 'importerInformation.owner.address', - title: information.labels.owner.address, - width: 'half', - required: true, - condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), - }), - buildTextField({ - id: 'importerInformation.owner.postCode', - title: information.labels.owner.postCode, - variant: 'number', - width: 'half', - required: true, - condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), - }), - buildPhoneField({ - id: 'importerInformation.owner.phone', - title: information.labels.owner.phone, - width: 'half', - required: true, - condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), - }), - buildTextField({ - id: 'importerInformation.owner.email', - title: information.labels.owner.email, - width: 'half', - variant: 'email', - required: true, - condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), - }), + buildCustomField( + { + id: 'importerInformation.custom', + title: '', + component: 'ChangeAnswers', + }, + { + sectionName: 'ownerInformation', + questionName: 'isOwnerOtherThanImporter', + person: 'owner', + }, + ), + buildCustomField( + { + id: 'importerInformation.custom2', + title: '', + component: 'ChangeAnswers', + }, + { + sectionName: 'operatorInformation', + questionName: 'hasOperator', + person: 'operator', + }, + ), ], }), ], diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OperatorInformation.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OperatorInformation.ts index 3a1003f48fa7..7eaebbb876b2 100644 --- a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OperatorInformation.ts +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OperatorInformation.ts @@ -4,10 +4,12 @@ import { buildSubSection, buildPhoneField, buildRadioField, + buildSelectField, } from '@island.is/application/core' import { information } from '../../../lib/messages' import { FormValue, NO, YES } from '@island.is/application/types' import { hasOperator } from '../../../utils/hasOperator' +import { postalCodes } from '@island.is/shared/utils' export const OperatorInformationSubSection = buildSubSection({ id: 'operatorInformation', @@ -39,6 +41,7 @@ export const OperatorInformationSubSection = buildSubSection({ title: information.labels.operator.name, width: 'half', required: true, + maxLength: 100, condition: (answer: FormValue) => hasOperator(answer), }), buildTextField({ @@ -54,14 +57,19 @@ export const OperatorInformationSubSection = buildSubSection({ title: information.labels.operator.address, width: 'half', required: true, + maxLength: 50, condition: (answer: FormValue) => hasOperator(answer), }), - buildTextField({ + buildSelectField({ id: 'operatorInformation.operator.postCode', - title: information.labels.operator.postCode, + title: information.labels.importer.postCode, width: 'half', required: true, - variant: 'number', + options: () => { + return postalCodes.map((code) => { + return { value: `${code}`, label: `${code}` } + }) + }, condition: (answer: FormValue) => hasOperator(answer), }), buildPhoneField({ @@ -77,6 +85,7 @@ export const OperatorInformationSubSection = buildSubSection({ width: 'half', required: true, variant: 'email', + maxLength: 250, condition: (answer: FormValue) => hasOperator(answer), }), ], diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OwnerInformation.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OwnerInformation.ts new file mode 100644 index 000000000000..5a2cb9b33b2d --- /dev/null +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/OwnerInformation.ts @@ -0,0 +1,107 @@ +import { + buildMultiField, + buildTextField, + buildSubSection, + buildPhoneField, + buildRadioField, + buildSelectField, + buildCustomField, +} from '@island.is/application/core' +import { information } from '../../../lib/messages' +import { FormValue, NO, YES } from '@island.is/application/types' +import { isOwnerOtherThanImporter } from '../../../utils/isOwnerOtherThanImporter' +import { postalCodes } from '@island.is/shared/utils' + +export const OwnerInformationSubSection = buildSubSection({ + id: 'ownerInformation', + title: information.labels.owner.sectionTitle, + children: [ + buildMultiField({ + id: 'ownerInformationMultiField', + title: information.labels.owner.title, + description: information.labels.owner.description, + children: [ + buildRadioField({ + id: 'ownerInformation.isOwnerOtherThanImporter', + title: information.labels.owner.isOwnerOtherThenImporter, + width: 'half', + defaultValue: NO, + options: [ + { + value: NO, + label: information.labels.radioButtons.radioOptionNo, + }, + { + value: YES, + label: information.labels.radioButtons.radioOptionYes, + }, + ], + }), + buildTextField({ + id: 'ownerInformation.owner.name', + title: information.labels.owner.name, + width: 'half', + required: true, + maxLength: 100, + condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), + }), + buildTextField({ + id: 'ownerInformation.owner.nationalId', + title: information.labels.owner.nationalId, + width: 'half', + required: true, + format: '######-####', + condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), + }), + buildTextField({ + id: 'ownerInformation.owner.address', + title: information.labels.owner.address, + width: 'half', + required: true, + maxLength: 50, + condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), + }), + buildSelectField({ + id: 'ownerInformation.owner.postCode', + title: information.labels.importer.postCode, + width: 'half', + required: true, + options: () => { + return postalCodes.map((code) => { + return { value: `${code}`, label: `${code}` } + }) + }, + condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), + }), + buildPhoneField({ + id: 'ownerInformation.owner.phone', + title: information.labels.owner.phone, + width: 'half', + required: true, + condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), + }), + buildTextField({ + id: 'ownerInformation.owner.email', + title: information.labels.owner.email, + width: 'half', + variant: 'email', + required: true, + maxLength: 250, + condition: (answer: FormValue) => isOwnerOtherThanImporter(answer), + }), + buildCustomField( + { + id: 'ownerInformation.custom', + title: '', + component: 'ChangeAnswers', + }, + { + sectionName: 'operatorInformation', + questionName: 'hasOperator', + person: 'operator', + }, + ), + ], + }), + ], +}) diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/index.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/index.ts index e86598f3a44e..076d1cb8e4e3 100644 --- a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/index.ts +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/InformationSection/index.ts @@ -2,9 +2,14 @@ import { buildSection } from '@island.is/application/core' import { information } from '../../../lib/messages' import { ImporterInformationSubSection } from './ImporterInformation' import { OperatorInformationSubSection } from './OperatorInformation' +import { OwnerInformationSubSection } from './OwnerInformation' export const InformationSection = buildSection({ id: 'informationSection', title: information.general.sectionTitle, - children: [ImporterInformationSubSection, OperatorInformationSubSection], + children: [ + ImporterInformationSubSection, + OwnerInformationSubSection, + OperatorInformationSubSection, + ], }) diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts index 11a5ed13c080..0bfe501432a7 100644 --- a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts @@ -10,6 +10,7 @@ import { } from '@island.is/application/core' import { information, machine } from '../../../lib/messages' import { NEW, USED } from '../../../shared/types' +import { getAllCountryCodes } from '@island.is/shared/utils' export const MachineBasicInformation = buildSubSection({ id: 'machineBasicInformation', @@ -36,11 +37,16 @@ export const MachineBasicInformation = buildSubSection({ titleVariant: 'h5', marginTop: 3, }), - buildTextField({ + buildSelectField({ id: 'machine.basicInformation.productionCountry', title: machine.labels.basicMachineInformation.productionCountry, width: 'half', required: true, + options: () => { + return getAllCountryCodes().map(({ name_is }) => { + return { value: `${name_is}`, label: `${name_is}` } + }) + }, }), buildTextField({ id: 'machine.basicInformation.productionYear', @@ -55,6 +61,7 @@ export const MachineBasicInformation = buildSubSection({ id: 'machine.basicInformation.productionNumber', title: machine.labels.basicMachineInformation.productionNumber, width: 'half', + maxLength: 50, required: true, }), buildSelectField({ @@ -116,13 +123,13 @@ export const MachineBasicInformation = buildSubSection({ id: 'machine.basicInformation.location', title: machine.labels.basicMachineInformation.location, width: 'half', - required: true, + maxLength: 255, }), buildTextField({ id: 'machine.basicInformation.cargoFileNumber', title: machine.labels.basicMachineInformation.cargoFileNumber, width: 'half', - required: true, + maxLength: 50, }), ], }), diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineLicensePlate.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineLicensePlate.ts index bfa6833f9f2a..f38f4ea950ce 100644 --- a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineLicensePlate.ts +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineLicensePlate.ts @@ -1,17 +1,18 @@ import { NO, YES, + buildAlertMessageField, buildMultiField, buildRadioField, buildSubSection, getValueViaPath, } from '@island.is/application/core' -import { information, licensePlate } from '../../../lib/messages' +import { information, licensePlate, overview } from '../../../lib/messages' import { plate110 } from '../../../assets/plates/plate-110-510' import { plate200 } from '../../../assets/plates/plate-200-280' import { plate155 } from '../../../assets/plates/plate-155-305' import { Plate } from '../../../shared/types' -import { canRegisterToTraffic } from '../../../utils' +import { canMaybeRegisterToTraffic, canRegisterToTraffic } from '../../../utils' export const MachineLicensePlate = buildSubSection({ id: 'streetRegistrationSection', @@ -73,6 +74,13 @@ export const MachineLicensePlate = buildSubSection({ return registerToTraffic === YES }, }), + buildAlertMessageField({ + id: 'streetRegistration.alertMessage', + title: overview.labels.alertMessageTitle, + message: overview.labels.alertMessageMessage, + alertType: 'warning', + condition: (answers) => canMaybeRegisterToTraffic(answers), + }), ], }), ], diff --git a/libs/application/templates/aosh/register-new-machine/src/lib/dataSchema.ts b/libs/application/templates/aosh/register-new-machine/src/lib/dataSchema.ts index 1b182d57d631..fce97d759459 100644 --- a/libs/application/templates/aosh/register-new-machine/src/lib/dataSchema.ts +++ b/libs/application/templates/aosh/register-new-machine/src/lib/dataSchema.ts @@ -30,8 +30,8 @@ const BasicInformationSchema = z.object({ markedCE: z.enum([YES, NO]), preRegistration: z.enum([YES, NO]).refine((v) => v.length > 0), isUsed: z.enum([NEW, USED]), - location: z.string().min(1), - cargoFileNumber: z.string().min(1), + location: z.string().optional(), + cargoFileNumber: z.string().optional(), }) const AboutMachineSchema = z.object({ @@ -51,9 +51,11 @@ const TechInfoSchema = z.object({ export const NewMachineAnswersSchema = z.object({ approveExternalData: z.boolean(), - importerInformation: z + importerInformation: z.object({ + importer: PersonInformationSchema, + }), + ownerInformation: z .object({ - importer: PersonInformationSchema, isOwnerOtherThanImporter: z.enum([YES, NO]), owner: RemovablePersonInformationSchema.optional(), }) diff --git a/libs/application/templates/aosh/register-new-machine/src/lib/messages/information.ts b/libs/application/templates/aosh/register-new-machine/src/lib/messages/information.ts index d02bdd680d63..bc97192f6bd1 100644 --- a/libs/application/templates/aosh/register-new-machine/src/lib/messages/information.ts +++ b/libs/application/templates/aosh/register-new-machine/src/lib/messages/information.ts @@ -26,11 +26,6 @@ export const information = { 'Et sed ut est aliquam proin elit sed. Nunc tellus lacus sed eu pulvinar. ', description: `Importer page description`, }, - isOwnerOtherThenImporter: { - id: 'aosh.rnm.application:information.labels.importer.isOwnerOtherThenImporter', - defaultMessage: 'Er eigandi annar en innflytjandi?', - description: `Is owner other than importer question label`, - }, name: { id: 'aosh.rnm.application:information.labels.importer.name', defaultMessage: 'Nafn', @@ -63,39 +58,54 @@ export const information = { }, }), owner: defineMessages({ + sectionTitle: { + id: 'aosh.rnm.application:information.labels.owner.sectionTitle', + defaultMessage: 'Eigandi', + description: `Owner section title`, + }, title: { id: 'aosh.rnm.application:information.labels.otherOwner.title', defaultMessage: 'Eigandi', description: `Owner title label`, }, + description: { + id: 'aosh.rnm.application:information.labels.owner.description', + defaultMessage: 'Skráðu viðeigandi upplýsingar', + description: `Owner page description`, + }, + isOwnerOtherThenImporter: { + id: 'aosh.rnm.application:information.labels.owner.isOwnerOtherThenImporter', + defaultMessage: 'Er eigandi annar en innflytjandi?', + description: `Is owner other than importer question label`, + }, name: { id: 'aosh.rnm.application:information.labels.otherOwner.name', - defaultMessage: 'Nafn eiganda', + defaultMessage: 'Nafn', description: `Owner name label`, }, nationalId: { id: 'aosh.rnm.application:information.labels.otherOwner.nationalId', - defaultMessage: 'Kennitala eiganda', + defaultMessage: 'Kennitala', description: `Owner nationalId label`, }, address: { id: 'aosh.rnm.application:information.labels.otherOwner.address', - defaultMessage: 'Heimilisfang eiganda', + defaultMessage: 'Heimilisfang', description: `Owner address label`, }, postCode: { id: 'aosh.rnm.application:information.labels.otherOwner.ostCode', - defaultMessage: 'Póstnúmer eiganda', + defaultMessage: 'Póstnúmer', description: `Owner post code label`, }, phone: { id: 'aosh.rnm.application:information.labels.otherOwner.phone', - defaultMessage: 'Símanúmer eiganda', + defaultMessage: 'Símanúmer', description: `Owner phone number label`, }, email: { id: 'aosh.rnm.application:information.labels.otherOwner.email', - defaultMessage: 'Netfang eiganda', + defaultMessage: 'Netfang', description: `Owner email label`, }, }), diff --git a/libs/application/templates/aosh/register-new-machine/src/lib/messages/overview.ts b/libs/application/templates/aosh/register-new-machine/src/lib/messages/overview.ts index 16671f99ef8b..eb94c8f29e2d 100644 --- a/libs/application/templates/aosh/register-new-machine/src/lib/messages/overview.ts +++ b/libs/application/templates/aosh/register-new-machine/src/lib/messages/overview.ts @@ -33,7 +33,7 @@ export const overview = { alertMessageMessage: { id: 'aosh.rnm.application:overview.labels.alertMessageMessage', defaultMessage: - 'Tæki í flokkum IA, IF, IM, JL og KG uppfylla oft ekki kröfur um gerð og búnað ökutækja og þarf að sækja sérstaklega um götuskráningu þeirra með því að hafa samband við þjónustuver Vinnueftirlitsins.', + 'Tæki í flokkum IA, IF, JL, IM, IB og KG uppfylla oft ekki kröfur um gerð og búnað ökutækja og þarf Vinnueftirlitið að taka þau sérstaklega út til að staðfesta að þau uppfylli kröfur um götuskráningu.', description: 'Overview alert message', }, editMessage: { diff --git a/libs/application/templates/aosh/register-new-machine/src/utils/canMaybeRegisterToTraffic.ts b/libs/application/templates/aosh/register-new-machine/src/utils/canMaybeRegisterToTraffic.ts index 5c701df5d5fd..cb5c23a4a526 100644 --- a/libs/application/templates/aosh/register-new-machine/src/utils/canMaybeRegisterToTraffic.ts +++ b/libs/application/templates/aosh/register-new-machine/src/utils/canMaybeRegisterToTraffic.ts @@ -1,7 +1,7 @@ import { getValueViaPath } from '@island.is/application/core' import { FormValue } from '@island.is/application/types' -const maybeAllowedCategories = ['IA', 'IF', 'IM', 'JL', 'KG'] +const maybeAllowedCategories = ['IA', 'IF', 'IM', 'JL', 'IB', 'KG'] export const canMaybeRegisterToTraffic = (answers: FormValue) => { const registrationNumberPrefix = getValueViaPath( diff --git a/libs/application/templates/aosh/register-new-machine/src/utils/canRegisterToTraffic.ts b/libs/application/templates/aosh/register-new-machine/src/utils/canRegisterToTraffic.ts index bff6f349ba38..207c87d34169 100644 --- a/libs/application/templates/aosh/register-new-machine/src/utils/canRegisterToTraffic.ts +++ b/libs/application/templates/aosh/register-new-machine/src/utils/canRegisterToTraffic.ts @@ -1,7 +1,20 @@ import { getValueViaPath } from '@island.is/application/core' import { FormValue } from '@island.is/application/types' -const allowedCategories = ['EA', 'EH', 'FH', 'HV', 'IB', 'IM', 'JF', 'KL'] +const allowedCategories = [ + 'EA', + 'EH', + 'FH', + 'HV', + 'IB', + 'JF', + 'KL', + 'IA', + 'IF', + 'IM', + 'JL', + 'KG', +] export const canRegisterToTraffic = (answers: FormValue) => { const registrationNumberPrefix = getValueViaPath( diff --git a/libs/application/templates/aosh/register-new-machine/src/utils/getTechnicalInformation.ts b/libs/application/templates/aosh/register-new-machine/src/utils/getTechnicalInformation.ts index 036507faa7e2..cd289a7906d2 100644 --- a/libs/application/templates/aosh/register-new-machine/src/utils/getTechnicalInformation.ts +++ b/libs/application/templates/aosh/register-new-machine/src/utils/getTechnicalInformation.ts @@ -1,11 +1,23 @@ import { getValueViaPath } from '@island.is/application/core' -import { FormValue } from '@island.is/application/types' +import { FormatMessage, FormValue } from '@island.is/application/types' import { TechInfo } from '../lib/dataSchema' +import { information } from '../lib/messages' -export const getTechnicalInformation = (answers: FormValue) => { +export const getTechnicalInformation = ( + answers: FormValue, + formatMessage: FormatMessage, +) => { const techInfo = getValueViaPath(answers, 'techInfo', []) as TechInfo[] return techInfo.map(({ label, value }) => { - return `${label}: ${value}` + return `${label}: ${ + value === undefined + ? '' + : value === 'yes' + ? formatMessage(information.labels.radioButtons.radioOptionYes) + : value === 'no' + ? formatMessage(information.labels.radioButtons.radioOptionNo) + : value + }` }) } diff --git a/libs/application/templates/aosh/register-new-machine/src/utils/isOwnerOtherThanImporter.ts b/libs/application/templates/aosh/register-new-machine/src/utils/isOwnerOtherThanImporter.ts index c46aaa057b93..b0ea8de462aa 100644 --- a/libs/application/templates/aosh/register-new-machine/src/utils/isOwnerOtherThanImporter.ts +++ b/libs/application/templates/aosh/register-new-machine/src/utils/isOwnerOtherThanImporter.ts @@ -4,7 +4,7 @@ import { FormValue, NO, YES } from '@island.is/application/types' export const isOwnerOtherThanImporter = (answers: FormValue) => { const isOwnerOtherThanImporter = getValueViaPath( answers, - 'importerInformation.isOwnerOtherThanImporter', + 'ownerInformation.isOwnerOtherThanImporter', NO, ) as typeof NO | typeof YES diff --git a/libs/clients/work-machines/src/clientConfig.json b/libs/clients/work-machines/src/clientConfig.json index 4116b87c3863..96ed8baa8343 100644 --- a/libs/clients/work-machines/src/clientConfig.json +++ b/libs/clients/work-machines/src/clientConfig.json @@ -585,6 +585,16 @@ }, "post": { "tags": ["Machines"], + "parameters": [ + { + "name": "X-Correlation-ID", + "in": "header", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "requestBody": { "content": { "application/json": { diff --git a/libs/shared/utils/src/index.ts b/libs/shared/utils/src/index.ts index 1e696acbf6ad..4e41c0d0c5d5 100644 --- a/libs/shared/utils/src/index.ts +++ b/libs/shared/utils/src/index.ts @@ -22,3 +22,4 @@ export * from './lib/date' export * from './lib/shouldLinkBeAnAnchorTag' export * from './lib/videoEmbed' export * from './lib/web' +export * from './lib/postalCodes' diff --git a/libs/shared/utils/src/lib/postalCodes.ts b/libs/shared/utils/src/lib/postalCodes.ts new file mode 100644 index 000000000000..d1eed270dce8 --- /dev/null +++ b/libs/shared/utils/src/lib/postalCodes.ts @@ -0,0 +1,17 @@ +export const postalCodes: number[] = [ + 101, 102, 103, 104, 105, 107, 108, 109, 110, 111, 112, 113, 116, 121, 123, + 124, 125, 127, 128, 129, 130, 132, 162, 170, 172, 190, 191, 200, 201, 202, + 203, 210, 212, 220, 221, 222, 225, 230, 232, 233, 235, 240, 241, 245, 246, + 250, 251, 260, 262, 270, 271, 276, 300, 301, 302, 310, 311, 320, 340, 341, + 345, 350, 351, 355, 356, 360, 370, 371, 380, 381, 400, 401, 410, 415, 416, + 420, 421, 425, 426, 430, 431, 450, 451, 460, 461, 465, 466, 470, 471, 500, + 510, 511, 512, 520, 524, 530, 531, 540, 541, 545, 546, 550, 551, 560, 561, + 565, 566, 570, 580, 581, 600, 601, 602, 603, 610, 611, 616, 620, 621, 625, + 626, 630, 640, 641, 645, 650, 660, 670, 671, 675, 676, 680, 681, 685, 686, + 690, 691, 700, 701, 710, 711, 715, 720, 721, 730, 731, 735, 736, 740, 741, + 750, 751, 755, 756, 760, 761, 765, 766, 780, 781, 785, 800, 801, 802, 810, + 815, 816, 820, 825, 840, 845, 846, 850, 851, 860, 861, 870, 871, 880, 881, + 900, 902, +] + +export default postalCodes From e77487b48f1161d77d055eabee2ca70b95b9318d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Mon, 16 Sep 2024 23:07:40 +0000 Subject: [PATCH 044/173] fix(j-s): Merge Email (#16011) * Fixes conclusion in merge email * Updates unit test --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../sendRulingNotifications.spec.ts | 4 +++- libs/judicial-system/formatters/src/lib/formatters.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts index 5dbf89cd491b..d6351b07b9f6 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts @@ -9,6 +9,7 @@ import { } from '@island.is/judicial-system/consts' import { CaseDecision, + CaseIndictmentRulingDecision, CaseState, CaseType, NotificationType, @@ -87,6 +88,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { type: CaseType.INDICTMENT, courtCaseNumber: '007-2022-07', court: { name: 'Héraðsdómur Reykjavíkur' }, + indictmentRulingDecision: CaseIndictmentRulingDecision.MERGE, prosecutor, } as Case @@ -101,7 +103,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { expect.objectContaining({ to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Máli lokið 007-2022-07', - html: `Máli 007-2022-07 hjá Héraðsdómi Reykjavíkur hefur verið lokið.

Niðurstaða: Ekki skráð

Skjöl málsins eru aðgengileg á ${expectedLink}yfirlitssíðu málsins í Réttarvörslugátt.`, + html: `Máli 007-2022-07 hjá Héraðsdómi Reykjavíkur hefur verið lokið.

Niðurstaða: Sameinað

Skjöl málsins eru aðgengileg á ${expectedLink}yfirlitssíðu málsins í Réttarvörslugátt.`, }), ) }) diff --git a/libs/judicial-system/formatters/src/lib/formatters.ts b/libs/judicial-system/formatters/src/lib/formatters.ts index b87283fc7de5..a25433fc0ffd 100644 --- a/libs/judicial-system/formatters/src/lib/formatters.ts +++ b/libs/judicial-system/formatters/src/lib/formatters.ts @@ -124,6 +124,8 @@ export const getHumanReadableCaseIndictmentRulingDecision = ( return 'Frávísun' case CaseIndictmentRulingDecision.CANCELLATION: return 'Niðurfelling máls' + case CaseIndictmentRulingDecision.MERGE: + return 'Sameinað' default: return 'Ekki skráð' } From e8480d9d852b7f371b6d0ecce128bfaf671abf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 17 Sep 2024 08:55:10 +0000 Subject: [PATCH 045/173] fix(j-s): Defender Case Info (#16024) --- .../src/app/modules/case/limitedAccessCase.service.ts | 1 + .../src/components/FormProvider/limitedAccessCase.graphql | 4 ++++ .../src/components/TagCaseState/TagCaseState.strings.ts | 4 ++-- .../Shared/IndictmentOverview/IndictmentOverview.tsx | 7 +++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index 0f784551d1f7..16c0fb14759f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -216,6 +216,7 @@ export const include: Includeable[] = [ required: false, where: { stringType: { [Op.in]: stringTypes } }, }, + { model: Case, as: 'mergeCase', attributes }, ] export const order: OrderItem[] = [ diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index bf9cadbb55d0..594219f55ff9 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -161,5 +161,9 @@ query LimitedAccessCase($input: CaseQueryInput!) { created eventType } + mergeCase { + id + courtCaseNumber + } } } diff --git a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts index 2fa55f78dae0..e79ae0ef00fa 100644 --- a/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts +++ b/apps/judicial-system/web/src/components/TagCaseState/TagCaseState.strings.ts @@ -73,9 +73,9 @@ export const strings = defineMessages({ description: 'Notað sem merki þegar mál í stöðu "Dómtekið" í málalista', }, completed: { - id: 'judicial.system.core:tag_case_state.completed', + id: 'judicial.system.core:tag_case_state.completed_v2', defaultMessage: - '{indictmentRulingDecision, select, RULING {Dómur} FINE {Viðurlagaákvörðun} DISMISSAL {Frávísun} CANCELLATION {Niðurfelling} other {Lokið}}', + '{indictmentRulingDecision, select, RULING {Dómur} FINE {Viðurlagaákvörðun} DISMISSAL {Frávísun} CANCELLATION {Niðurfelling} MERGE {Sameinað} other {Lokið}}', description: 'Notað sem merki þegar mál í stöðu "Dómþulur" í málalista', }, recalled: { diff --git a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx index 7151e95d1b3a..cb2da97042f7 100644 --- a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx +++ b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx @@ -26,6 +26,7 @@ import { UserContext, } from '@island.is/judicial-system-web/src/components' import { + CaseIndictmentRulingDecision, CaseState, IndictmentDecision, UserRole, @@ -110,8 +111,10 @@ const IndictmentOverview: FC = () => { {caseIsClosed ? ( ) : ( From ebbb3473b9f735dba2d36af510eec768ea8a3f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Tue, 17 Sep 2024 11:31:29 +0000 Subject: [PATCH 046/173] feat(j-s): Add civil claim file upload (#16004) * Add case file category civil claim * Add civil claim to defender view * Remove console.log * Fix tests --- .../modules/case/limitedAccessCase.service.ts | 1 + .../modules/file/guards/caseFileCategory.ts | 1 + .../limitedAccessViewCaseFileGuard.spec.ts | 1 + .../IndictmentCaseFilesList.tsx | 21 +++++++++++++++++ .../CaseFiles/CaseFiles.strings.ts | 5 ++++ .../Indictments/CaseFiles/CaseFiles.tsx | 23 +++++++++++++++++++ libs/judicial-system/types/src/lib/file.ts | 1 + 7 files changed, 53 insertions(+) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index 16c0fb14759f..aa51cb9a6cda 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -193,6 +193,7 @@ export const include: Includeable[] = [ CaseFileCategory.CASE_FILE, CaseFileCategory.PROSECUTOR_CASE_FILE, CaseFileCategory.DEFENDANT_CASE_FILE, + CaseFileCategory.CIVIL_CLAIM, ], }, }, diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts index 020af77d5a4f..0e46c408ac7e 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts @@ -31,6 +31,7 @@ const defenderCaseFileCategoriesForIndictmentCases = [ CaseFileCategory.CASE_FILE, CaseFileCategory.PROSECUTOR_CASE_FILE, CaseFileCategory.DEFENDANT_CASE_FILE, + CaseFileCategory.CIVIL_CLAIM, ] const prisonAdminCaseFileCategories = [ diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts index b46d12eda442..13d0b175f3e6 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts @@ -160,6 +160,7 @@ describe('Limited Access View Case File Guard', () => { CaseFileCategory.CASE_FILE, CaseFileCategory.PROSECUTOR_CASE_FILE, CaseFileCategory.DEFENDANT_CASE_FILE, + CaseFileCategory.CIVIL_CLAIM, ] describe.each(allowedCaseFileCategories)( diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index aad0eb781b67..10d1b3b71023 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -5,7 +5,9 @@ import { AnimatePresence } from 'framer-motion' import { Box, Text } from '@island.is/island-ui/core' import { isCompletedCase, + isDefenceUser, isDistrictCourtUser, + isProsecutionUser, isPublicProsecutor, isPublicProsecutorUser, isTrafficViolationCase, @@ -99,6 +101,9 @@ const IndictmentCaseFilesList: FC = ({ file.category === CaseFileCategory.PROSECUTOR_CASE_FILE || file.category === CaseFileCategory.DEFENDANT_CASE_FILE, ) + const civilClaims = cf?.filter( + (file) => file.category === CaseFileCategory.CIVIL_CLAIM, + ) return ( <> @@ -225,6 +230,22 @@ const IndictmentCaseFilesList: FC = ({ )}
) : null} + {civilClaims && + civilClaims.length > 0 && + (isDistrictCourtUser(user) || + isProsecutionUser(user) || + isDefenceUser(user)) && ( + + + {formatMessage(caseFiles.civilClaimSection)} + + + + )} {uploadedCaseFiles && uploadedCaseFiles.length > 0 && ( diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.strings.ts b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.strings.ts index b721b42e5c5e..554259c44bc3 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.strings.ts +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.strings.ts @@ -42,6 +42,11 @@ export const caseFiles = defineMessages({ defaultMessage: 'Önnur gögn', description: 'Titill á önnur gögn hluta á dómskjalaskjá í ákærum.', }, + civilClaimSection: { + id: 'judicial.system.core:indictments.case_files.civil_claim_section', + defaultMessage: 'Bótakröfur', + description: 'Titill á bótakröfur hluta á dómskjalaskjá í ákærum.', + }, criminalRecordUpdateSection: { id: 'judicial.system.core:indictments.case_files.criminal_record_update_section', defaultMessage: 'Tilkynning til sakaskrár', diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx index 91f8ef99a563..5a6864a0152e 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFiles/CaseFiles.tsx @@ -172,6 +172,29 @@ const CaseFiles = () => { onRetry={(file) => handleRetry(file, updateUploadFile)} /> + + + file.category === CaseFileCategory.CIVIL_CLAIM, + )} + accept={Object.values(fileExtensionWhitelist)} + header={formatMessage(strings.caseFiles.inputFieldLabel)} + buttonLabel={formatMessage(strings.caseFiles.buttonLabel)} + onChange={(files) => + handleUpload( + addUploadFiles(files, { + category: CaseFileCategory.CIVIL_CLAIM, + }), + updateUploadFile, + ) + } + onRemove={(file) => handleRemove(file, removeUploadFile)} + onRetry={(file) => handleRetry(file, updateUploadFile)} + /> + {isTrafficViolationCaseCheck && ( Date: Tue, 17 Sep 2024 12:02:57 +0000 Subject: [PATCH 047/173] feat(web): Latest Generic List Items content type - Don't allow clicking on cards if there isn't a 'see more page' (#16031) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../LatestGenericListItems/LatestGenericListItems.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/components/GenericList/LatestGenericListItems/LatestGenericListItems.tsx b/apps/web/components/GenericList/LatestGenericListItems/LatestGenericListItems.tsx index 2ea75b950012..c8dcb640d93d 100644 --- a/apps/web/components/GenericList/LatestGenericListItems/LatestGenericListItems.tsx +++ b/apps/web/components/GenericList/LatestGenericListItems/LatestGenericListItems.tsx @@ -19,8 +19,6 @@ export const LatestGenericListItems = ({ return null } - const itemsAreClickable = slice.genericList?.itemType === 'Clickable' - // Only allow organization subpage links as is let seeMoreLinkHref = '' if ( @@ -34,6 +32,11 @@ export const LatestGenericListItems = ({ ]).href } + const itemsAreClickable = + slice.genericList?.itemType === 'Clickable' && + slice.seeMoreLinkText && + seeMoreLinkHref + return ( From 4c8d8d1f4c6a76f224f5cfc6d6bdfe82e5e298c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3n=20Bjarni=20=C3=93lafsson?= <92530555+jonbjarnio@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:15:39 +0000 Subject: [PATCH 048/173] fix(ojoi): Signature HTML and comments (#16032) * Signature HTML now updates when users interacts with signature fields * Get comments now working properly * Posting comments now works properly --- .../src/models/getComments.input.ts | 4 +- .../src/models/getComments.response.ts | 32 +- .../src/models/postComment.input.ts | 4 +- .../src/components/comments/AddComment.tsx | 34 +- .../src/components/comments/Comment.tsx | 20 +- .../src/components/signatures/Chairman.tsx | 21 +- .../components/signatures/CommitteeMember.tsx | 41 +- .../src/components/signatures/Institution.tsx | 25 + .../components/signatures/RegularMember.tsx | 40 +- .../src/fields/Comments.tsx | 35 +- .../src/fields/Preview.tsx | 4 +- .../src/fields/Signatures.tsx | 4 +- .../src/graphql/queries.ts | 39 +- .../src/hooks/useComments.ts | 8 +- .../src/lib/messages/comments.ts | 7 + .../src/lib/utils.ts | 11 +- .../application/src/clientConfig.json | 473 ++++++++---------- .../src/lib/ojoiApplicationClient.service.ts | 15 +- 18 files changed, 433 insertions(+), 384 deletions(-) diff --git a/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.input.ts b/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.input.ts index fdbe6f6a03ab..e7280e28e484 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.input.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.input.ts @@ -1,7 +1,7 @@ -import { Field, InputType } from '@nestjs/graphql' +import { Field, ID, InputType } from '@nestjs/graphql' @InputType('OfficialJournalOfIcelandApplicationGetCommentsInput') export class GetCommentsInput { - @Field() + @Field(() => ID) id!: string } diff --git a/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.response.ts b/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.response.ts index cf472295d940..fac5a5b5a0c0 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.response.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/models/getComments.response.ts @@ -1,5 +1,16 @@ -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, ID, ObjectType } from '@nestjs/graphql' +@ObjectType('OfficialJournalOfIcelandApplicationEntity') +export class CaseCommentEntity { + @Field(() => ID) + id!: string + + @Field() + title!: string + + @Field() + slug!: string +} @ObjectType('OfficialJournalOfIcelandApplicationCommentTask') export class CaseCommentTask { @Field(() => String, { nullable: true }) @@ -8,15 +19,16 @@ export class CaseCommentTask { @Field(() => String, { nullable: true }) to!: string | null - @Field() - title!: string + @Field(() => CaseCommentEntity) + title!: CaseCommentEntity + @Field(() => String, { nullable: true }) comment!: string | null } @ObjectType('OfficialJournalOfIcelandApplicationComment') export class CaseComment { - @Field() + @Field(() => ID) id!: string @Field() @@ -25,14 +37,14 @@ export class CaseComment { @Field() internal!: boolean - @Field() - type!: string + @Field(() => CaseCommentEntity) + type!: CaseCommentEntity - @Field() - caseStatus!: string + @Field(() => CaseCommentEntity) + status!: CaseCommentEntity - @Field() - state!: string + @Field(() => String, { nullable: true }) + state!: string | null @Field(() => CaseCommentTask) task!: CaseCommentTask diff --git a/libs/api/domains/official-journal-of-iceland-application/src/models/postComment.input.ts b/libs/api/domains/official-journal-of-iceland-application/src/models/postComment.input.ts index 8a9f6929199d..a226457cad5a 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/models/postComment.input.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/models/postComment.input.ts @@ -1,8 +1,8 @@ -import { Field, InputType } from '@nestjs/graphql' +import { Field, ID, InputType } from '@nestjs/graphql' @InputType('OfficialJournalOfIcelandApplicationPostCommentInput') export class PostCommentInput { - @Field(() => String, { description: 'Application ID' }) + @Field(() => ID) id!: string @Field() diff --git a/libs/application/templates/official-journal-of-iceland/src/components/comments/AddComment.tsx b/libs/application/templates/official-journal-of-iceland/src/components/comments/AddComment.tsx index 9676ed6fb1aa..88a07a730f20 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/comments/AddComment.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/comments/AddComment.tsx @@ -8,18 +8,23 @@ import { import { useState } from 'react' import { comments } from '../../lib/messages/comments' import { useLocale } from '@island.is/localization' -import { useComments } from '../../hooks/useComments' +import { AddCommentVariables } from '../../hooks/useComments' +import { ApolloError } from '@apollo/client' type Props = { - applicationId: string + addComment: (variables: AddCommentVariables, cb?: () => void) => void + addCommentLoading?: boolean + addCommentSuccess?: boolean + addCommentError?: ApolloError } -export const AddComment = ({ applicationId }: Props) => { +export const AddComment = ({ + addComment, + addCommentError, + addCommentLoading, + addCommentSuccess, +}: Props) => { const { formatMessage: f } = useLocale() - const { addComment, addCommentLoading, addCommentSuccess } = useComments({ - applicationId, - }) - const [comment, setComment] = useState('') const onAddComment = () => { @@ -31,13 +36,14 @@ export const AddComment = ({ applicationId }: Props) => { return ( - {addCommentSuccess === false && ( - - )} + {addCommentSuccess === false || + (addCommentError && ( + + ))} type?: 'sent' | 'received' } @@ -22,16 +23,16 @@ export const Comment = ({ }: Props) => { const Wrapper = as - const { formatMessage } = useLocale() + const { formatMessage: f } = useLocale() const daysAgo = date ? countDaysAgo(new Date(date)) : null - const many = formatMessage(comments.dates.xDaysAgo, { + const many = f(comments.dates.xDaysAgo, { days: daysAgo, }) - const yesterDay = formatMessage(comments.dates.yesterday) - const today = formatMessage(comments.dates.today) + const yesterDay = f(comments.dates.yesterday) + const today = f(comments.dates.today) const msg = daysAgo === 0 ? today : daysAgo === 1 ? yesterDay : many @@ -50,11 +51,10 @@ export const Comment = ({ )} - {from && ( - - {from} {task && `${task}`} - - )} + + {from ? from : f(comments.unknownUser.name)}{' '} + {task && `${task}`} + {comment} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx index 91b23f17dcaf..e95a9acbb706 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx @@ -3,7 +3,11 @@ import { useApplication } from '../../hooks/useUpdateApplication' import { useLocale } from '@island.is/localization' import { signatures } from '../../lib/messages/signatures' import { InputFields } from '../../lib/types' -import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils' +import { + getCommitteeAnswers, + getEmptyMember, + getSingleSignatureMarkup, +} from '../../lib/utils' import { memberItemSchema } from '../../lib/dataSchema' import { SignatureMember } from './Member' import set from 'lodash/set' @@ -29,9 +33,22 @@ export const Chairman = ({ applicationId, member }: Props) => { ) if (signature) { + const additionalSignature = + application.answers.signatures?.additionalSignature?.committee + const chairman = { ...signature.chairman, [key]: value } + + const html = getSingleSignatureMarkup( + { + ...signature, + }, + additionalSignature, + chairman, + ) + const updatedCommitteeSignature = { ...signature, - chairman: { ...signature.chairman, [key]: value }, + chairman: chairman, + html: html, } const updatedSignatures = set( diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx index c153a116f289..a269ded1ae8b 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx @@ -5,12 +5,15 @@ import { useLocale } from '@island.is/localization' import { signatures } from '../../lib/messages/signatures' import { InputFields } from '../../lib/types' import set from 'lodash/set' -import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils' +import { + getCommitteeAnswers, + getEmptyMember, + getSingleSignatureMarkup, +} from '../../lib/utils' import { memberItemSchema } from '../../lib/dataSchema' import { SignatureMember } from './Member' import * as z from 'zod' import { RemoveCommitteeMember } from './RemoveComitteeMember' -import { getValueViaPath } from '@island.is/application/core' type Props = { applicationId: string @@ -40,18 +43,32 @@ export const CommitteeMember = ({ ) if (signature) { - const updatedCommitteeSignature = { - ...signature, - members: signature?.members?.map((m, i) => { - if (i === memberIndex) { - return { - ...m, - [key]: value, - } + const additionalSignature = + application.answers.signatures?.additionalSignature?.committee + const members = signature?.members?.map((m, i) => { + if (i === memberIndex) { + return { + ...m, + [key]: value, } + } + + return m + }) - return m - }), + const html = getSingleSignatureMarkup( + { + ...signature, + members, + }, + additionalSignature, + signature.chairman, + ) + + const updatedCommitteeSignature = { + ...signature, + members: members, + html: html, } const updatedSignatures = set( diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx index 06a94dbf7502..5a70d5b1512e 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx @@ -20,6 +20,7 @@ import { getCommitteeAnswers, getRegularAnswers, getSignatureDefaultValues, + getSingleSignatureMarkup, isCommitteeSignature, isRegularSignature, } from '../../lib/utils' @@ -61,9 +62,20 @@ export const InstitutionSignature = ({ if (isRegularSignature(signature)) { const updatedRegularSignature = signature?.map((signature, index) => { if (index === signatureIndex) { + const additionalSignature = + application.answers.signatures?.additionalSignature?.regular + const html = getSingleSignatureMarkup( + { + ...signature, + [key]: value, + }, + additionalSignature, + ) + return { ...signature, [key]: value, + html: html, } } @@ -80,11 +92,24 @@ export const InstitutionSignature = ({ } if (isCommitteeSignature(signature)) { + const chairman = signature.chairman + const additionalSignature = + application.answers.signatures?.additionalSignature?.committee + const html = getSingleSignatureMarkup( + { + ...signature, + [key]: value, + }, + additionalSignature, + chairman, + ) + const updatedCommitteeSignature = set( currentAnswers, InputFields.signature[type], { ...signature, + html: html, [key]: value, }, ) diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx index 01ce1ba0ed42..d1e48b57c55c 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx @@ -5,7 +5,12 @@ import { useLocale } from '@island.is/localization' import { signatures } from '../../lib/messages/signatures' import { InputFields } from '../../lib/types' import set from 'lodash/set' -import { getEmptyMember, getRegularAnswers } from '../../lib/utils' +import { + getEmptyMember, + getRegularAnswers, + getSignaturesMarkup, + getSingleSignatureMarkup, +} from '../../lib/utils' import { memberItemSchema } from '../../lib/dataSchema' import { SignatureMember } from './Member' import * as z from 'zod' @@ -42,18 +47,31 @@ export const RegularMember = ({ if (signature) { const updatedRegularSignature = signature.map((s, index) => { if (index === si) { - return { - ...s, - members: s.members?.map((member, memberIndex) => { - if (memberIndex === mi) { - return { - ...member, - [key]: value, - } + const additionalSignature = + application.answers.signatures?.additionalSignature?.regular + const members = s.members?.map((member, memberIndex) => { + if (memberIndex === mi) { + return { + ...member, + [key]: value, } + } + + return member + }) - return member - }), + const html = getSingleSignatureMarkup( + { + ...s, + members: members, + }, + additionalSignature, + ) + + return { + ...s, + html: html, + members: members, } } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx index e5a25426b2fb..fce2af1b6dab 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx @@ -18,7 +18,15 @@ import { AddComment } from '../components/comments/AddComment' export const Comments = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() - const { comments, loading, error } = useComments({ + const { + comments, + loading, + error, + addComment, + addCommentLoading, + addCommentSuccess, + addCommentError, + } = useComments({ applicationId: application.id, }) @@ -45,7 +53,7 @@ export const Comments = ({ application }: OJOIFieldBaseProps) => { message={f(errorMessages.fetchCommentsFailedMessage)} /> )} - {!showCommentsList && ( + {!showCommentsList && !error && ( { background="blue100" > ({ - task: comment.task.title, - comment: comment.task.comment as string, - from: comment.task.from ?? undefined, - date: comment.createdAt, - type: 'received', // TODO: Implement sent comments - }))} + comments={comments?.map((comment) => { + return { + task: comment.task.title.title, + comment: comment.task.comment, + from: comment.task.from ?? undefined, + date: comment.createdAt, + type: 'received', // TODO: Implement sent comments + } + })} /> )} - + ) diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx index 7385687ab936..834cfc539513 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx @@ -12,7 +12,7 @@ import { signatureConfig } from '../components/htmlEditor/config/signatureConfig import { OJOIFieldBaseProps } from '../lib/types' import { useLocale } from '@island.is/localization' import { HTMLText } from '@island.is/regulations-tools/types' -import { getAdvertMarkup, getSignatureMarkup } from '../lib/utils' +import { getAdvertMarkup, getSignaturesMarkup } from '../lib/utils' import { SignatureTypes } from '../lib/constants' import { useApplication } from '../hooks/useUpdateApplication' import { advert, error, preview } from '../lib/messages' @@ -39,7 +39,7 @@ export const Preview = ({ application }: OJOIFieldBaseProps) => { ) } - const signatureMarkup = getSignatureMarkup({ + const signatureMarkup = getSignaturesMarkup({ signatures: currentApplication.answers.signatures, type: currentApplication.answers.misc?.signatureType as SignatureTypes, }) diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx index 881ad65f48ef..719da33a0b8e 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx @@ -10,7 +10,7 @@ import { RegularSignature } from '../components/signatures/Regular' import { useApplication } from '../hooks/useUpdateApplication' import set from 'lodash/set' import { HTMLEditor } from '../components/htmlEditor/HTMLEditor' -import { getSignatureMarkup } from '../lib/utils' +import { getSignaturesMarkup } from '../lib/utils' export const Signatures = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() @@ -71,7 +71,7 @@ export const Signatures = ({ application }: OJOIFieldBaseProps) => { { id: applicationId, }, }, + fetchPolicy: 'no-cache', }, ) @@ -45,7 +46,7 @@ export const useComments = ({ applicationId }: Props) => { }, }) - const addComment = (variables: AddCommentVariables) => { + const addComment = (variables: AddCommentVariables, cb?: () => void) => { addCommentMutation({ variables: { input: { @@ -54,12 +55,15 @@ export const useComments = ({ applicationId }: Props) => { }, }, }) + + cb && cb() } return { comments: data?.officialJournalOfIcelandApplicationGetComments.comments, loading, error, + refetchComments: refetch, addComment, addCommentLoading, addCommentError, diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts index c204aad33321..25a033d7e8e8 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts @@ -41,6 +41,13 @@ export const comments = { description: 'Post comment failed message', }, }), + unknownUser: defineMessages({ + name: { + id: 'ojoi.application:comments.unknownUser.name', + defaultMessage: 'Óþekktur notandi', + description: 'Unknown user name', + }, + }), dates: defineMessages({ today: { id: 'ojoi.application:comments.dates.today', diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts b/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts index f588b640eeb8..976a51e21517 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts @@ -5,6 +5,7 @@ import { committeeSignatureSchema, memberItemSchema, partialSchema, + regularSignatureItemSchema, regularSignatureSchema, } from './dataSchema' import { getValueViaPath } from '@island.is/application/core' @@ -242,7 +243,7 @@ const signatureTemplate = ( return `${markup}${additionalMarkup}` as HTMLText } -export const getSignatureMarkup = ({ +export const getSignaturesMarkup = ({ signatures, type, }: { @@ -311,3 +312,11 @@ export const parseZodIssue = (issue: z.ZodCustomIssue) => { message: issue?.params as MessageDescriptor, } } + +export const getSingleSignatureMarkup = ( + signature: z.infer, + additionalSignature?: string, + chairman?: z.infer, +) => { + return signatureTemplate([signature], additionalSignature, chairman) +} diff --git a/libs/clients/official-journal-of-iceland/application/src/clientConfig.json b/libs/clients/official-journal-of-iceland/application/src/clientConfig.json index 86aa18f66b74..ea30281a3f28 100644 --- a/libs/clients/official-journal-of-iceland/application/src/clientConfig.json +++ b/libs/clients/official-journal-of-iceland/application/src/clientConfig.json @@ -389,343 +389,217 @@ }, "required": ["price"] }, - "ApplicationRequirements": { + "ApplicationCommunicationChannel": { "type": "object", "properties": { - "approveExternalData": { + "email": { + "type": "string", + "example": "test@test.is", + "description": "Email of the communication channel" + }, + "phone": { "type": "string", - "enum": ["yes", "no"], - "example": "yes", - "description": "Has the applicant approved the requirements" + "example": "555 5555", + "description": "Phone number of the communication channel" } }, - "required": ["approveExternalData"] + "required": ["email", "phone"] }, "ApplicationAdvert": { "type": "object", "properties": { - "department": { + "departmentId": { "type": "string", - "example": "b783c4d5-6e78-9f01-2g34-h56i7j8k9l0m", - "description": "Id of the selected department for the application advert" + "example": "a12c3d4e-5f67-8h90-1i23-j45k6l7m8n9o0", + "description": "Id of the selected department" }, - "type": { + "typeId": { "type": "string", - "example": "a71ka2b3-4c56-7d89-0e12-3f45g6h7i8j9", - "description": "Id of the selected type for the application advert" + "example": "a12c3d4e-5f67-8h90-1i23-j45k6l7m8n9o0", + "description": "Id of the selected type" }, "title": { "type": "string", - "example": "GJALDSKRÁ fyrir hundahald í Reykjavík", - "description": "Title of the application advert" + "example": "a12c3d4e-5f67-8h90-1i23-j45k6l7m8n9o0", + "description": "Title of the advert" }, - "document": { + "html": { "type": "string", - "description": "HTML string of the application advert" + "example": "a12c3d4e-5f67-8h90-1i23-j45k6l7m8n9o0", + "description": "HTML contents of the advert" }, - "template": { + "requestedDate": { "type": "string", - "description": "Selected template for the application advert" + "example": "2021-04-01T00:00:00.000Z", + "description": "Request advert publcation date" + }, + "categories": { "type": "array", "items": { "type": "string" } }, + "channels": { + "description": "Communication channels", + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationCommunicationChannel" + } }, - "subType": { + "message": { "type": "string", - "example": "b781ks2-3c45-6d78-9e01-2f34g5h6i7j8", - "description": "Id of the selected subType for the application advert, only when type is \"Reglugerð\"" + "example": "Some message to the admins", + "description": "Message to the admins of the advert" } }, "required": [ - "department", - "type", + "departmentId", + "typeId", "title", - "document", - "template", - "subType" + "html", + "requestedDate", + "categories", + "channels", + "message" ] }, - "ApplicationPreview": { + "ApplicationMisc": { "type": "object", "properties": { - "document": { + "signatureType": { + "type": "string", + "enum": ["regular", "committee"] + } + }, + "required": ["signatureType"] + }, + "AdditionalSignature": { + "type": "object", + "properties": { + "regular": { + "type": "string", + "description": "Message to the regular" + }, + "committee": { "type": "string", - "example": "

GJALDSKRÁ

", - "description": "HTML string of the advert with signature" + "description": "Message to the committee" } }, - "required": ["document"] + "required": ["regular", "committee"] }, "ApplicationSignatureMember": { "type": "object", "properties": { - "above": { + "name": { "type": "string", "description": "Name of the member" }, + "before": { "type": "string", - "example": "F.h.r", - "description": "Text above the name of the signature" + "description": "Text before the name member" }, - "name": { + "below": { "type": "string", - "example": "Jón Jónsson", - "description": "Name of the signature" + "description": "Text below the name member" }, - "after": { + "above": { "type": "string", - "example": "ráðherra", - "description": "Text after the name of the signature" + "description": "Text above the name member" }, - "below": { + "after": { "type": "string", - "example": "Text below the name of the signature", - "description": "borgarstjóri" + "description": "Text after the name member" } }, - "required": ["above", "name", "after", "below"] + "required": ["name", "before", "below", "above", "after"] }, - "ApplicationRegularSignature": { + "ApplicationSignature": { "type": "object", "properties": { "institution": { "type": "string", - "example": "Dómsmálaráðuneytið", - "description": "Institution/place of the signature" + "description": "Institution of the signature" }, "date": { "type": "string", - "example": "2021-04-01T00:00:00.000Z", - "description": "Date of the signature" + "description": "Date when the signature was signed" }, "members": { - "description": "Member of the signature", + "description": "Members of the signature", "type": "array", "items": { "$ref": "#/components/schemas/ApplicationSignatureMember" } + }, + "html": { + "type": "string", + "description": "The html contents of the signature" } }, - "required": ["institution", "date", "members"] + "required": ["institution", "date", "members", "html"] }, "ApplicationCommitteeSignature": { "type": "object", "properties": { "institution": { "type": "string", - "example": "Dómsmálaráðuneytið", - "description": "Institution/place of the signature" + "description": "Institution of the signature" }, "date": { "type": "string", - "example": "2021-04-01T00:00:00.000Z", - "description": "Date of the signature" + "description": "Date when the signature was signed" }, "members": { - "description": "Member of the signature", + "description": "Members of the signature", "type": "array", "items": { "$ref": "#/components/schemas/ApplicationSignatureMember" } - } - }, - "required": ["institution", "date", "members"] - }, - "ApplicationSignature": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "committee", - "description": "Signature type of the application" - }, - "signature": { - "type": "string", - "example": "

Jón Jónsson

", - "description": "HTML string of the signature" }, - "additional": { + "html": { "type": "string", - "example": "Dagur B. Eggertsson", - "description": "Additional name of the signature" + "description": "The html contents of the signature" }, - "regular": { - "description": "Regular signature of the application", - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationRegularSignature" - } - }, - "committee": { - "description": "Committee signature of the application", + "chairman": { + "description": "The title of the committee", "allOf": [ - { "$ref": "#/components/schemas/ApplicationCommitteeSignature" } + { "$ref": "#/components/schemas/ApplicationSignatureMember" } ] } }, - "required": ["type", "signature", "additional", "regular", "committee"] + "required": ["institution", "date", "members", "html", "chairman"] }, - "ApplicationAttachmentsFileSchema": { + "ApplicationSignatures": { "type": "object", "properties": { - "name": { - "type": "string", - "example": "filename.doc", - "description": "Name of the attachment" - }, - "key": { - "type": "string", - "example": "key", - "description": "Key of the attachment" - } - }, - "required": ["name", "key"] - }, - "ApplicationAttachments": { - "type": "object", - "properties": { - "files": { - "description": "List of attachments", - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAttachmentsFileSchema" - } - }, - "fileNames": { - "type": "string", - "example": "document", - "description": "Selected department for the application attachment, should be \"document\" or \"additions\"" - } - }, - "required": ["files", "fileNames"] - }, - "ApplicationContentCategories": { - "type": "object", - "properties": { - "label": { - "type": "string", - "example": "Gæludýr", - "description": "Label of the category" - }, - "value": { - "type": "string", - "example": "b619j2k3-4l56-7m89-0n12-3o45p6q7r8s9", - "description": "Id of the selected category" - } - }, - "required": ["label", "value"] - }, - "ApplicationCommunicationChannels": { - "type": "object", - "properties": { - "email": { - "type": "string", - "example": "email", - "description": "Selected communication channel" + "additionalSignature": { + "example": "Some message to the applicant", + "description": "Message to the applicant", + "allOf": [{ "$ref": "#/components/schemas/AdditionalSignature" }] }, - "phone": { - "type": "string", - "example": "5555555", - "description": "Phone number of the communication channel" - } - }, - "required": ["email", "phone"] - }, - "ApplicationPublishing": { - "type": "object", - "properties": { - "date": { - "type": "string", - "example": "2021-04-01T00:00:00.000Z", - "description": "Requested publishing date" - }, - "fastTrack": { - "type": "string", - "enum": ["yes", "no"], - "example": "yes", - "description": "Request fast track for the advert" - }, - "contentCategories": { - "description": "List of selected categories", - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationContentCategories" - } - }, - "communicationChannels": { - "description": "Selected communication channels", + "regular": { + "description": "Regular signature", "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationCommunicationChannels" - } + "items": { "$ref": "#/components/schemas/ApplicationSignature" } }, - "message": { - "type": "string", - "example": "Some message..", - "description": "Message for the publisher" - } - }, - "required": [ - "date", - "fastTrack", - "contentCategories", - "communicationChannels", - "message" - ] - }, - "ApplicationOriginalFiles": { - "type": "object", - "properties": { - "files": { - "description": "List of original files", - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAttachmentsFileSchema" - } + "committee": { + "description": "Committee signature", + "allOf": [ + { "$ref": "#/components/schemas/ApplicationCommitteeSignature" } + ] } }, - "required": ["files"] + "required": ["additionalSignature", "regular", "committee"] }, "ApplicationAnswers": { "type": "object", "properties": { - "requirements": { - "example": "true", - "description": "Has the applicant approved the external data", - "allOf": [ - { "$ref": "#/components/schemas/ApplicationRequirements" } - ] - }, "advert": { - "description": "Application advert", + "description": "Answers for the advert application", "allOf": [{ "$ref": "#/components/schemas/ApplicationAdvert" }] }, - "preview": { - "description": "Contents of the full document", - "allOf": [{ "$ref": "#/components/schemas/ApplicationPreview" }] + "misc": { + "description": "Misc answers", + "allOf": [{ "$ref": "#/components/schemas/ApplicationMisc" }] }, - "signature": { - "description": "Application signature", - "allOf": [{ "$ref": "#/components/schemas/ApplicationSignature" }] - }, - "additionsAndDocuments": { - "description": "Application attachments", - "allOf": [{ "$ref": "#/components/schemas/ApplicationAttachments" }] - }, - "publishing": { - "description": "Application publishing", - "allOf": [{ "$ref": "#/components/schemas/ApplicationPublishing" }] - }, - "original": { - "description": "Application original files", - "allOf": [ - { "$ref": "#/components/schemas/ApplicationOriginalFiles" } - ] + "signatures": { + "description": "Signature answers", + "allOf": [{ "$ref": "#/components/schemas/ApplicationSignatures" }] } }, - "required": [ - "requirements", - "advert", - "preview", - "signature", - "additionsAndDocuments", - "publishing", - "original" - ] + "required": ["advert", "misc", "signatures"] }, "Application": { "type": "object", @@ -786,7 +660,6 @@ "description": "Application answers", "allOf": [{ "$ref": "#/components/schemas/ApplicationAnswers" }] }, - "externalData": { "type": "object" }, "listed": { "type": "boolean", "example": true, @@ -816,7 +689,6 @@ "name", "applicantActors", "answers", - "externalData", "listed", "prunedAt", "pruned" @@ -829,20 +701,67 @@ }, "required": ["application"] }, - "CaseCommentTask": { + "CaseCommentType": { "type": "object", "properties": { - "from": { + "id": { "type": "string", - "description": "From who or what initied the task, used by client to show who inited the task.", - "example": "Ármann", - "nullable": true + "description": "The title of the case comment type" }, - "to": { + "title": { "type": "string", - "description": "To whom or what the task is assigned to.", - "example": "Pálina J", - "nullable": true + "enum": [ + "submit", + "assign", + "assign-self", + "update", + "comment", + "message" + ], + "description": "The title of the case comment type" + }, + "slug": { + "type": "string", + "description": "The slug of the case comment type" + } + }, + "required": ["id", "title", "slug"] + }, + "CaseStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "title": { + "type": "string", + "enum": [ + "Innsent", + "Grunnvinnsla", + "Yfirlestur", + "Tilbúið", + "Útgefið", + "Tekið úr birtingu", + "Birtingu hafnað" + ], + "example": "Innsent", + "description": "Status of the case" + }, + "slug": { + "type": "string", + "example": "innsent", + "description": "Slug of the case staus" + } + }, + "required": ["id", "title", "slug"] + }, + "CaseCommentTitle": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The title of the case comment type" }, "title": { "type": "string", @@ -854,8 +773,33 @@ "gerir athugasemd.", "skráir skilaboð" ], - "example": "Innsent af:", - "description": "Title for the task action" + "description": "The title of the case comment type" + }, + "slug": { + "type": "string", + "description": "The slug of the case comment type" + } + }, + "required": ["id", "title", "slug"] + }, + "CaseCommentTask": { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "From who or what initied the task, used by client to show who inited the task.", + "example": "Ármann", + "nullable": true + }, + "to": { + "type": "string", + "description": "To whom or what the task is assigned to.", + "example": "Pálina J", + "nullable": true + }, + "title": { + "description": "Title for the task action", + "allOf": [{ "$ref": "#/components/schemas/CaseCommentTitle" }] }, "comment": { "type": "string", @@ -885,31 +829,12 @@ "example": false }, "type": { - "type": "string", - "enum": [ - "submit", - "assign", - "assign_self", - "update", - "comment", - "message" - ], - "example": "comment", - "description": "Type of the case task." + "description": "Type of the case task.", + "allOf": [{ "$ref": "#/components/schemas/CaseCommentType" }] }, - "caseStatus": { - "type": "string", - "enum": [ - "Innsent", - "Grunnvinnsla", - "Yfirlestur", - "Tilbúið", - "Útgefið", - "Tekið úr birtingu", - "Birtingu hafnað" - ], - "example": "Innsent", - "description": "Status of case when comment was added." + "status": { + "description": "Status of case when comment was added.", + "allOf": [{ "$ref": "#/components/schemas/CaseStatus" }] }, "state": { "type": "string", @@ -931,7 +856,7 @@ "createdAt", "internal", "type", - "caseStatus", + "status", "state", "task" ] diff --git a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.service.ts b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.service.ts index c897634300ae..85e1ecb83ee9 100644 --- a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.service.ts +++ b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.service.ts @@ -32,7 +32,18 @@ export class OfficialJournalOfIcelandApplicationClientService { async getComments( params: GetCommentsRequest, ): Promise { - return await this.ojoiApplicationApi.getComments(params) + try { + return await this.ojoiApplicationApi.getComments(params) + } catch (error) { + console.log(error) + this.logger.error('Failed to get comments', { + error, + applicationId: params.id, + category: LOG_CATEGORY, + }) + + throw error + } } async postComment(params: PostCommentRequest): Promise { @@ -40,7 +51,7 @@ export class OfficialJournalOfIcelandApplicationClientService { await this.ojoiApplicationApi.postComment(params) return true } catch (error) { - this.logger.error('Failed to post comment', { + this.logger.error(`Failed to post comment: ${error.message}`, { error, applicationId: params.id, category: LOG_CATEGORY, From c2b2a7e91e38c842e2d6cba1f76b08adc869af96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:31:21 +0000 Subject: [PATCH 049/173] feat(web): Pension Calculator - Tax calculation translation (#16033) --- .../SocialInsuranceAdministration/translationStrings.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts b/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts index 9f0e6098d253..84752d10a013 100644 --- a/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts +++ b/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts @@ -622,6 +622,11 @@ export const translationStrings = defineMessages({ defaultMessage: 'Samtals frá TR fyrir skatt', description: 'Niðurstöðuskjár, Samtals frá TR fyrir skatt', }, + 'REIKNH.FRADRSKATTUR': { + id: 'web.pensionCalculator:REIKNH.FRADRSKATTUR', + defaultMessage: 'Frádreginn skattur af öðrum greiðslum', + description: 'Niðurstöðuskjár, Frádreginn skattur af öðrum greiðslum', + }, 'REIKNH.FRADRSKATTURTR1': { id: 'web.pensionCalculator:REIKNH.FRADRSKATTURTR1', defaultMessage: 'Frádreginn skattur TR (1. og 2. skattþrep)', From e7fc25345600a5e1626271b9987b2b6a5592bd94 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:06:36 +0000 Subject: [PATCH 050/173] fix(service-portal): parliamentary refactor signee view (#16030) * fix(service-portal): refactor signee view * cleanup * chore: nx format:write update dirty files --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../signature-collection/src/lib/messages.ts | 7 +- .../screens/Parliamentary/OwnerView/index.tsx | 22 +-- .../OwnerView/modals/DeletePerson/index.tsx | 42 ----- .../OwnerView/modals/LookupPerson/index.tsx | 104 ------------- .../Parliamentary/SignedList/index.tsx | 146 ------------------ .../src/screens/Parliamentary/index.tsx | 7 +- .../screens/Presidential/OwnerView/index.tsx | 6 +- .../screens/Presidential/SigneeView/index.tsx | 135 ---------------- .../src/screens/Presidential/index.tsx | 2 +- .../SignedList/index.tsx | 57 ++++--- .../SigneeView/index.tsx | 14 +- .../signature-collection/src/skeletons.tsx | 2 +- 12 files changed, 57 insertions(+), 487 deletions(-) delete mode 100644 libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/DeletePerson/index.tsx delete mode 100644 libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/LookupPerson/index.tsx delete mode 100644 libs/service-portal/signature-collection/src/screens/Parliamentary/SignedList/index.tsx delete mode 100644 libs/service-portal/signature-collection/src/screens/Presidential/SigneeView/index.tsx rename libs/service-portal/signature-collection/src/screens/{Presidential => shared}/SignedList/index.tsx (76%) rename libs/service-portal/signature-collection/src/screens/{Parliamentary => shared}/SigneeView/index.tsx (92%) diff --git a/libs/service-portal/signature-collection/src/lib/messages.ts b/libs/service-portal/signature-collection/src/lib/messages.ts index ac1781136297..7cad88479236 100644 --- a/libs/service-portal/signature-collection/src/lib/messages.ts +++ b/libs/service-portal/signature-collection/src/lib/messages.ts @@ -16,7 +16,7 @@ export const m = defineMessages({ pageDescriptionSignee: { id: 'sp.signatureCollection:pageDescriptionSignee', defaultMessage: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur imperdiet, dui eget iaculis vehicula, purus nibh lobortis urna, sit amet dignissim lacus metus non arcu.', + 'Aðeins er hægt að mæla með einu framboði. Hægt er að afturkalla meðmæli þangað til söfnun lokar og mæla með öðrum frambjóðanda ef vill.', description: '', }, createListButton: { @@ -29,6 +29,11 @@ export const m = defineMessages({ defaultMessage: 'Forsetakosningar 2024', description: '', }, + collectionTitleParliamentary: { + id: 'sp.signatureCollection:collectionTitleParliamentary', + defaultMessage: 'Alþingiskosningar', + description: '', + }, myListsDescription: { id: 'sp.signatureCollection:myListsDescription', defaultMessage: 'Yfirlit safnana sem þú hefur stofnað', 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 5337b7b86c23..229c72a0afe0 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 @@ -9,17 +9,10 @@ import { import { constituencies } from '../../../lib/constants' import { useNavigate } from 'react-router-dom' import { SignatureCollectionPaths } from '../../../lib/paths' -import LookupPerson from './modals/LookupPerson' 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 { useGetListsForOwner } from '../../../hooks' import { SignatureCollection } from '@island.is/api/schema' const OwnerView = ({ @@ -29,7 +22,6 @@ const OwnerView = ({ }) => { const navigate = useNavigate() const { formatMessage } = useLocale() - const { userInfo: user } = useAuth() const { listsForOwner, loadingOwnerLists } = useGetListsForOwner( currentCollection?.id || '', ) @@ -92,28 +84,18 @@ const OwnerView = ({ {formatMessage(m.supervisors) + ' '}
-
{formatMessage(m.personName)} {formatMessage(m.personNationalId)} - - {'Nafni Nafnason'} + {'Nafni Nafnason'} {'010130-3019'} - - - - - diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/DeletePerson/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/DeletePerson/index.tsx deleted file mode 100644 index fa5cb31a1ca2..000000000000 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/DeletePerson/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useState } from 'react' -import { Box, Button, Text } from '@island.is/island-ui/core' -import { Modal } from '@island.is/service-portal/core' -import { useLocale } from '@island.is/localization' -import { m } from '../../../../../lib/messages' - -const DeletePersonModal = () => { - const { formatMessage } = useLocale() - const [modalIsOpen, setModalIsOpen] = useState(false) - - return ( - - - setModalIsOpen(false)} - label={''} - > - - {formatMessage(m.deleteManager)} - - - {formatMessage(m.deleteManagerDescription)} - - - - - - - ) -} - -export default DeletePersonModal diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/LookupPerson/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/LookupPerson/index.tsx deleted file mode 100644 index d7a96d4285b0..000000000000 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/modals/LookupPerson/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useEffect, useState } from 'react' -import { Box, Stack, Button, Text, Input } from '@island.is/island-ui/core' -import { Modal } from '@island.is/service-portal/core' -import { useLocale } from '@island.is/localization' -import { useIdentityQuery } from '@island.is/service-portal/graphql' -import { InputController } from '@island.is/shared/form-fields' -import { useForm } from 'react-hook-form' -import { m } from '../../../../../lib/messages' - -interface LookupPersonProps { - collectionId: string - title: string -} - -const LookupPerson = ({ collectionId, title }: LookupPersonProps) => { - const { formatMessage } = useLocale() - const { control } = useForm() - - const [modalIsOpen, setModalIsOpen] = useState(false) - const [nationalIdInput, setNationalIdInput] = useState('') - const [nationalIdNotFound, setNationalIdNotFound] = useState(false) - const [name, setName] = useState('') - - const { data, loading } = useIdentityQuery({ - variables: { - input: { - nationalId: nationalIdInput, - }, - }, - skip: nationalIdInput.length !== 10, - }) - - useEffect(() => { - if (!loading) { - if (nationalIdInput.length === 10 && data?.identity?.name) { - setName(data.identity.name) - } else { - setName('') - setNationalIdNotFound(nationalIdInput.length === 10) - if (nationalIdInput.length !== 10) { - setNationalIdInput('') - } - } - } - }, [nationalIdInput, loading]) - - return ( - - - setModalIsOpen(false)} - label={''} - > - - {title} - - - { - setNationalIdInput(e.target.value.replace(/\W/g, '')) - }} - error={ - nationalIdNotFound - ? formatMessage(m.nationalIdInvalid) - : undefined - } - loading={loading} - /> - - - - - - - - ) -} - -export default LookupPerson 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 deleted file mode 100644 index 7317f2874728..000000000000 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/SignedList/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -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/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx index e84d9872468d..6f37054fbefa 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx @@ -1,5 +1,5 @@ import { Box } from '@island.is/island-ui/core' -import { useLocale } from '@island.is/localization' +import { useLocale, useNamespaces } from '@island.is/localization' import { EmptyState, IntroHeader, @@ -7,11 +7,12 @@ import { } from '@island.is/service-portal/core' import { m } from '../../lib/messages' import OwnerView from './OwnerView' -import SigneeView from './SigneeView' +import SigneeView from '../shared/SigneeView' import { useGetCurrentCollection, useIsOwner } from '../../hooks' import { Skeleton } from '../../skeletons' const SignatureListsParliamentary = () => { + useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() const { isOwner, loadingIsOwner } = useIsOwner() @@ -21,7 +22,7 @@ const SignatureListsParliamentary = () => { return ( {/* Signed list */} - {!user?.profile.actor && } + {!user?.profile.actor && ( + + )} {/* Candidate created lists */} diff --git a/libs/service-portal/signature-collection/src/screens/Presidential/SigneeView/index.tsx b/libs/service-portal/signature-collection/src/screens/Presidential/SigneeView/index.tsx deleted file mode 100644 index b8e97a633d7d..000000000000 --- a/libs/service-portal/signature-collection/src/screens/Presidential/SigneeView/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - ActionCard, - AlertMessage, - Box, - Button, - Stack, - Text, -} from '@island.is/island-ui/core' -import { useLocale, useNamespaces } from '@island.is/localization' -import { EmptyState, IntroHeader } 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 -}) => { - useNamespaces('sp.signatureCollection') - const { userInfo: user } = useAuth() - - const { formatMessage } = useLocale() - const { signedLists, loadingSignedLists } = useGetSignedList() - const { listsForUser, loadingUserLists } = useGetListsForUser( - currentCollection?.id, - ) - - return ( - - {!user?.profile.actor && !loadingSignedLists && !loadingUserLists ? ( - - {currentCollection?.isActive && ( - - )} - {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 ? ( - - ) : ( - - )} - - ) -} - -export default SigneeView diff --git a/libs/service-portal/signature-collection/src/screens/Presidential/index.tsx b/libs/service-portal/signature-collection/src/screens/Presidential/index.tsx index f8e427108044..c6cd4e314d7a 100644 --- a/libs/service-portal/signature-collection/src/screens/Presidential/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Presidential/index.tsx @@ -1,11 +1,11 @@ import { Box } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' -import SigneeView from './SigneeView' import OwnerView from './OwnerView' import { useGetCurrentCollection, useIsOwner } from '../../hooks' import { EmptyState, IntroHeader } from '@island.is/service-portal/core' import { m } from '../../lib/messages' import { CollectionType } from '../../lib/constants' +import SigneeView from '../shared/SigneeView' const SignatureLists = () => { useNamespaces('sp.signatureCollection') diff --git a/libs/service-portal/signature-collection/src/screens/Presidential/SignedList/index.tsx b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx similarity index 76% rename from libs/service-portal/signature-collection/src/screens/Presidential/SignedList/index.tsx rename to libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx index 410d4f148153..760557b5d41e 100644 --- a/libs/service-portal/signature-collection/src/screens/Presidential/SignedList/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx @@ -8,11 +8,16 @@ import format from 'date-fns/format' import { useMutation } from '@apollo/client' import { unSignList } from '../../../hooks/graphql/mutations' import { + SignatureCollection, SignatureCollectionSignedList, SignatureCollectionSuccess, } from '@island.is/api/schema' -const SignedList = () => { +const SignedList = ({ + currentCollection, +}: { + currentCollection: SignatureCollection +}) => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() const [modalIsOpen, setModalIsOpen] = useState(false) @@ -37,7 +42,7 @@ const SignedList = () => { await unSign().then(({ data }) => { if ( ( - data as any as { + data as unknown as { signatureCollectionUnsign: SignatureCollectionSuccess } ).signatureCollectionUnsign.success @@ -69,7 +74,11 @@ const SignedList = () => { ? formatMessage(m.signedTime) : formatMessage(m.uploadedTime) } ${format(new Date(list.signedDate), 'dd.MM.yyyy')}`} - text={formatMessage(m.collectionTitle)} + text={ + currentCollection.isPresidential + ? formatMessage(m.collectionTitle) + : formatMessage(m.collectionTitleParliamentary) + } cta={ list.canUnsign ? { @@ -112,27 +121,29 @@ const SignedList = () => { initialVisibility={false} onCloseModal={() => setModalIsOpen(false)} > - - {formatMessage(m.unSignList)} - - - {formatMessage(m.unSignModalMessage)} - - - + + diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx b/libs/service-portal/signature-collection/src/screens/shared/SigneeView/index.tsx similarity index 92% rename from libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx rename to libs/service-portal/signature-collection/src/screens/shared/SigneeView/index.tsx index 78cb34ff9c49..e349fb02c637 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/shared/SigneeView/index.tsx @@ -2,14 +2,12 @@ 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' @@ -23,7 +21,6 @@ const SigneeView = ({ currentCollection: SignatureCollection }) => { const { userInfo: user } = useAuth() - const { formatMessage } = useLocale() const { signedLists, loadingSignedLists } = useGetSignedList() const { listsForUser, loadingUserLists } = useGetListsForUser( @@ -46,7 +43,7 @@ const SigneeView = ({ )} {/* Signed list */} - + {/* Other available lists */} @@ -63,12 +60,11 @@ const SigneeView = ({ key={list.id} backgroundColor="white" heading={list.title} - eyebrow={ - formatMessage(m.endTime) + - ' ' + - format(new Date(list.endTime), 'dd.MM.yyyy') + text={ + currentCollection.isPresidential + ? formatMessage(m.collectionTitle) + : formatMessage(m.collectionTitleParliamentary) } - text={formatMessage(m.collectionTitle)} cta={ new Date(list.endTime) > new Date() && !list.maxReached ? { diff --git a/libs/service-portal/signature-collection/src/skeletons.tsx b/libs/service-portal/signature-collection/src/skeletons.tsx index e6ac9fba5edb..421cde75ab69 100644 --- a/libs/service-portal/signature-collection/src/skeletons.tsx +++ b/libs/service-portal/signature-collection/src/skeletons.tsx @@ -3,7 +3,7 @@ import { Box, SkeletonLoader } from '@island.is/island-ui/core' export const Skeleton = () => { return ( - + ) } From f0e526ebb60c43f1d464478f74b2155a532b2be1 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:27:40 +0000 Subject: [PATCH 051/173] feat(web): Add default header for HSN organization (#16035) 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 873637509e02..a3d50ebb3d0c 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -317,7 +317,15 @@ export const OrganizationHeader: React.FC< /> ) case 'hsn': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Tue, 17 Sep 2024 14:29:08 +0000 Subject: [PATCH 052/173] chore(license-service): Disable cache when not prod (#16036) * chore: init * chore: console --- libs/services/license/src/lib/licenseCache.provider.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/services/license/src/lib/licenseCache.provider.ts b/libs/services/license/src/lib/licenseCache.provider.ts index aee2a04eca46..254fcaa38cf5 100644 --- a/libs/services/license/src/lib/licenseCache.provider.ts +++ b/libs/services/license/src/lib/licenseCache.provider.ts @@ -9,12 +9,16 @@ export const LICENSE_SERVICE_CACHE_MANAGER_PROVIDER = export const LicenseCacheProvider: FactoryProvider = { provide: LICENSE_SERVICE_CACHE_MANAGER_PROVIDER, scope: LazyDuringDevScope, - useFactory: (licenseServiceConfig: ConfigType) => - createRedisCacheManager({ + useFactory: (licenseServiceConfig: ConfigType) => { + if (process.env.NODE_ENV !== 'production') { + return undefined + } + return createRedisCacheManager({ name: 'license_service_cache_manager', nodes: licenseServiceConfig.redis.nodes, ssl: licenseServiceConfig.redis.ssl, ttl: licenseServiceConfig.redis.cacheTtl, - }), + }) + }, inject: [LicenseConfig.KEY], } From 323fd5691d9ad37e33b7b31f51830b25c38babf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:01:02 +0000 Subject: [PATCH 053/173] fix(web): Pension Calculator - Remove space in translation string keys (#16037) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../SocialInsuranceAdministration/translationStrings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts b/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts index 84752d10a013..7050d578e080 100644 --- a/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts +++ b/apps/web/screens/Organization/SocialInsuranceAdministration/translationStrings.ts @@ -843,12 +843,12 @@ export const translationStrings = defineMessages({ defaultMessage: 'Meðlag', description: 'Niðurstöðuskjár, Meðlag', }, - 'REIKNH.PERSAFSLMINNA ': { + 'REIKNH.PERSAFSLMINNA': { id: 'web.pensionCalculator:REIKNH.PERSAFSLMINNA', defaultMessage: 'Persónuafsláttur', description: 'Niðurstöðuskjár, Persónuafsláttur minna', }, - 'REIKNH.SAMANBBOTAFLOKKUR ': { + 'REIKNH.SAMANBBOTAFLOKKUR': { id: 'web.pensionCalculator:REIKNH.SAMANBBOTAFLOKKUR', defaultMessage: 'Samanburðarbótaflokkur við eldra kerfi', description: 'Niðurstöðuskjár, Samanburðarbótaflokkur við eldra kerfi ', From 43a8006ace926ac7ddc723c45321d290d240228e Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:32:18 +0000 Subject: [PATCH 054/173] feat(web): Add default header for HMS organization (#16028) * Add default header for HMS organization * Reorder return from getThemeConfig --- .../Wrapper/OrganizationWrapper.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index a3d50ebb3d0c..610a1adccc19 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -183,6 +183,10 @@ export const getThemeConfig = ( ? 'organization' : 'default' + if (lightThemes.includes(theme ?? '') || usingDefaultHeader) { + return { themeConfig: { footerVersion } } + } + if (blueberryThemes.includes(theme ?? '')) return { themeConfig: { @@ -191,7 +195,6 @@ export const getThemeConfig = ( footerVersion, }, } - if (darkThemes.includes(theme ?? '')) { return { themeConfig: { @@ -202,10 +205,6 @@ export const getThemeConfig = ( } } - if (lightThemes.includes(theme ?? '') || usingDefaultHeader) { - return { themeConfig: { footerVersion } } - } - return { themeConfig: { headerColorScheme: 'white', @@ -479,13 +478,20 @@ export const OrganizationHeader: React.FC< /> ) case 'hms': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( ) - case 'rikissaksoknari': return ( Date: Wed, 18 Sep 2024 10:18:01 +0000 Subject: [PATCH 055/173] fix: add back in feature flag check for passkeys on logout (#16047) --- apps/native/app/src/stores/auth-store.ts | 40 +++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/native/app/src/stores/auth-store.ts b/apps/native/app/src/stores/auth-store.ts index f9693962a082..83e21a4d5ddf 100644 --- a/apps/native/app/src/stores/auth-store.ts +++ b/apps/native/app/src/stores/auth-store.ts @@ -68,21 +68,32 @@ const getAppAuthConfig = () => { } } -const clearPasskey = async () => { +const clearPasskey = async (userNationalId?: string) => { // Clear passkey if exists - preferencesStore.setState({ - hasCreatedPasskey: false, - hasOnboardedPasskeys: false, - lastUsedPasskey: 0, - }) - - const client = await getApolloClientAsync() - try { - await client.mutate({ - mutation: DeletePasskeyDocument, + const isPasskeyEnabled = await featureFlagClient?.getValueAsync( + 'isPasskeyEnabled', + false, + userNationalId ? { identifier: userNationalId } : undefined, + ) + + if (isPasskeyEnabled) { + preferencesStore.setState({ + hasCreatedPasskey: false, + hasOnboardedPasskeys: false, + lastUsedPasskey: 0, }) - } catch (e) { - console.error('Failed to delete passkey', e) + + const client = await getApolloClientAsync() + try { + await client.mutate< + DeletePasskeyMutation, + DeletePasskeyMutationVariables + >({ + mutation: DeletePasskeyDocument, + }) + } catch (e) { + console.error('Failed to delete passkey', e) + } } } @@ -188,7 +199,8 @@ export const authStore = create((set, get) => ({ notificationsStore.getState().reset() // Clear passkey if exists - await clearPasskey() + const userNationalId = get().userInfo?.nationalId + await clearPasskey(userNationalId) const appAuthConfig = getAppAuthConfig() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion From 560a5c13f8184863dd07ee7ddffd656b9de468f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Wed, 18 Sep 2024 10:27:46 +0000 Subject: [PATCH 056/173] chore(native-app): bump version to 1.4.2 (#16041) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/android/app/build.gradle | 2 +- apps/native/app/ios/IslandApp/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/native/app/android/app/build.gradle b/apps/native/app/android/app/build.gradle index bcdf28ccfabb..c7e8ce717a86 100644 --- a/apps/native/app/android/app/build.gradle +++ b/apps/native/app/android/app/build.gradle @@ -103,7 +103,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode getMyVersionCode(143) - versionName "1.4.1" + versionName "1.4.2" manifestPlaceholders = [ appAuthRedirectScheme: "is.island.app" // project.config.get("BUNDLE_ID_ANDROID") ] diff --git a/apps/native/app/ios/IslandApp/Info.plist b/apps/native/app/ios/IslandApp/Info.plist index 24d36d07c197..bc7ae29607a2 100644 --- a/apps/native/app/ios/IslandApp/Info.plist +++ b/apps/native/app/ios/IslandApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleSignature ???? CFBundleURLTypes From 825530d55ff55a3a67c8dcf9dfce3fd2bbde1b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Wed, 18 Sep 2024 10:35:03 +0000 Subject: [PATCH 057/173] fix(native-app): use replace instead of replaceAll (#16049) * fix: use replace instead of replaceAll * fix: add missing px suffix * fix: update regex --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../app/src/screens/document-detail/document-detail.tsx | 4 ++-- .../app/src/screens/vehicles/components/vehicle-item.tsx | 2 +- 2 files changed, 3 insertions(+), 3 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 e88c3deae612..0dbd13a225f1 100644 --- a/apps/native/app/src/screens/document-detail/document-detail.tsx +++ b/apps/native/app/src/screens/document-detail/document-detail.tsx @@ -45,7 +45,7 @@ const PdfWrapper = styled.View` flex: 1; background-color: ${dynamicColor('background')}; ` -const regexForBr = /
*\\?>/g +const regexForBr = //gi // Styles for html documents const useHtmlStyles = () => { @@ -377,7 +377,7 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{ 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( + `${htmlStyles}${Document.content?.value.replace( regexForBr, '', )}` 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 0ac33169fe71..100dc377366b 100644 --- a/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx +++ b/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx @@ -15,7 +15,7 @@ type VehicleListItem = NonNullable< >[0] const Cell = styled(TouchableHighlight)` - margin-bottom: ${({ theme }) => theme.spacing[2]}; + margin-bottom: ${({ theme }) => theme.spacing[2]}px; border-radius: ${({ theme }) => theme.border.radius.extraLarge}; ` From 378e8a9bc31e30667fc6fd17faab510b44c8a6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Wed, 18 Sep 2024 10:48:25 +0000 Subject: [PATCH 058/173] feat(regulations-admin): ministry basic + bugfixes (#16042) * Add ministry to basic screen + minor bugfixes * Remove console log * Minor refactor * cleanup * cleanup --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../regulation-documents.controller.ts | 1 + .../src/components/EditBasics.tsx | 55 +++++++++++++++---- .../src/components/EditSignature.tsx | 2 +- .../components/impacts/EditCancellation.tsx | 6 +- .../src/components/impacts/EditChange.tsx | 6 +- .../regulations-admin/src/lib/messages.ts | 4 ++ .../src/state/actionHandlers.ts | 4 ++ .../regulations-admin/src/state/types.ts | 4 ++ .../src/state/useDraftingState.ts | 6 +- libs/regulations/src/lib/types-admin.ts | 1 + 10 files changed, 75 insertions(+), 14 deletions(-) diff --git a/apps/download-service/src/app/modules/regulation-documents/regulation-documents.controller.ts b/apps/download-service/src/app/modules/regulation-documents/regulation-documents.controller.ts index 9fd2e66917a3..0887c6c656ef 100644 --- a/apps/download-service/src/app/modules/regulation-documents/regulation-documents.controller.ts +++ b/apps/download-service/src/app/modules/regulation-documents/regulation-documents.controller.ts @@ -76,6 +76,7 @@ export class RegulationDocumentsController { comments: draftRegulation.comments, name: draftRegulation.name, publishedDate: draftRegulation.idealPublishDate, + ministry: draftRegulation.ministry, } const documentResponse = diff --git a/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx b/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx index 7b20d6e862b4..b6f79ed5fec0 100644 --- a/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx +++ b/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx @@ -8,9 +8,10 @@ import { Button, AlertMessage, AlertBanner, + Select, } from '@island.is/island-ui/core' import { EditorInput } from './EditorInput' -import { editorMsgs as msg, errorMsgs } from '../lib/messages' +import { editorMsgs as msg, errorMsgs, m } from '../lib/messages' import { useLocale } from '@island.is/localization' import { Appendixes } from './Appendixes' import { MagicTextarea } from './MagicTextarea' @@ -32,7 +33,7 @@ const updateText = export const EditBasics = () => { const t = useLocale().formatMessage - const { draft, actions } = useDraftingState() + const { draft, actions, ministries } = useDraftingState() const [editorKey, setEditorKey] = useState('initial') const [titleError, setTitleError] = useState(undefined) const [hasUpdated, setHasUpdated] = useState(false) @@ -162,13 +163,15 @@ export const EditBasics = () => { label={t(msg.text)} startExpanded={startTextExpanded} > - - - + {draft.type.value === RegulationDraftTypes.amending ? ( + + + + ) : undefined} { {' '} - {draft.signedDocumentUrl.value && ( + {draft.signedDocumentUrl.value ? ( { readOnly /> + ) : ( + ministries.length > 0 && + !draft.signatureText.value && ( + + ) +} + +export default InputName diff --git a/apps/judicial-system/web/src/components/Inputs/types.d.ts b/apps/judicial-system/web/src/components/Inputs/types.d.ts new file mode 100644 index 000000000000..67b9da399a10 --- /dev/null +++ b/apps/judicial-system/web/src/components/Inputs/types.d.ts @@ -0,0 +1,13 @@ +export interface InputProps { + onBlur: (value: string) => void + onChange?: (value: string) => void + disabled?: boolean + value?: string + required?: boolean + + // Use `label` to overwrite the default label + label?: string + + // Use `placeholder` to overwrite the default placeholder text + placeholder?: string +} diff --git a/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx index f807d897ead7..6fff397b7f80 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx @@ -16,6 +16,7 @@ import { import { isIndictmentCase } from '@island.is/judicial-system/types' import { core } from '@island.is/judicial-system-web/messages' import { BlueBox } from '@island.is/judicial-system-web/src/components' +import InputName from '@island.is/judicial-system-web/src/components/Inputs/InputName' import { Defendant, Gender, @@ -71,9 +72,6 @@ const DefendantInfo: FC = (props) => { useState('') const [nationalIdNotFound, setNationalIdNotFound] = useState(false) - const [accusedNameErrorMessage, setAccusedNameErrorMessage] = - useState('') - const [accusedAddressErrorMessage, setAccusedAddressErrorMessage] = useState('') @@ -97,7 +95,6 @@ const DefendantInfo: FC = (props) => { } if (personData && personData.items && personData.items.length > 0) { - setAccusedNameErrorMessage('') setAccusedAddressErrorMessage('') setNationalIdErrorMessage('') setIsGenderAndCitizenshipDisabled(false) @@ -121,7 +118,6 @@ const DefendantInfo: FC = (props) => { } if (businessData && businessData.items && businessData.items.length > 0) { - setAccusedNameErrorMessage('') setAccusedAddressErrorMessage('') setNationalIdErrorMessage('') setIsGenderAndCitizenshipDisabled(true) @@ -257,45 +253,25 @@ const DefendantInfo: FC = (props) => { )}
- { - removeErrorMessageIfValid( - ['empty'], - evt.target.value, - accusedNameErrorMessage, - setAccusedNameErrorMessage, - ) - + onBlur={(value) => + onChange({ + caseId: workingCase.id, + defendantId: defendant.id, + name: value.trim(), + }) + } + onChange={(value) => { updateDefendantState( { caseId: workingCase.id, defendantId: defendant.id, - name: evt.target.value, + name: value, }, setWorkingCase, ) }} - onBlur={(evt) => { - validateAndSetErrorMessage( - ['empty'], - evt.target.value, - setAccusedNameErrorMessage, - ) - - onChange({ - caseId: workingCase.id, - defendantId: defendant.id, - name: evt.target.value.trim(), - }) - }} required /> From dffc3471363060901982810559e9e6c48033c0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20M=C3=A1r=20Atlason?= <54210288+saevarma@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:17:42 +0000 Subject: [PATCH 084/173] feat(portals-admin-ids-admin): Add LegalRepresentative delegation type (#16069) * Add missing translation for legal representation delegation type in IDS Admin * Refactor delegation type filtering from client and scope creation to account for LegalRepresentative as superuser only. * Update patch super user delegation type check. Add tests. * Handle legal representative in scope patch. Add tests. * Hide it from the UI. * Fix duplicated message id --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../app/v2/clients/test/me-clients.spec.ts | 73 +++++++++++++++++++ .../src/app/v2/scopes/test/me-scopes.spec.ts | 56 +++++++++++++- libs/auth-api-lib/src/index.ts | 1 + .../clients/admin/admin-clients.service.ts | 35 +++++---- .../admin/dto/admin-create-client.dto.ts | 5 +- .../admin/dto/admin-patch-client.dto.ts | 5 +- .../resources/admin/admin-scope.service.ts | 16 ++-- .../admin/dto/admin-create-scope.dto.ts | 13 +++- .../admin/dto/admin-patch-scope.dto.ts | 5 +- .../src/lib/resources/utils/filters.ts | 19 +++++ .../utils/personalRepresentativeFilter.ts | 14 ---- .../admin/ids-admin/src/lib/messages.ts | 36 +++++++-- .../screens/Client/components/Delegation.tsx | 6 +- .../components/PermissionDelegations.tsx | 6 +- 14 files changed, 231 insertions(+), 59 deletions(-) create mode 100644 libs/auth-api-lib/src/lib/resources/utils/filters.ts delete mode 100644 libs/auth-api-lib/src/lib/resources/utils/personalRepresentativeFilter.ts diff --git a/apps/services/auth/admin-api/src/app/v2/clients/test/me-clients.spec.ts b/apps/services/auth/admin-api/src/app/v2/clients/test/me-clients.spec.ts index 795e60e7e759..58235d12f2b1 100644 --- a/apps/services/auth/admin-api/src/app/v2/clients/test/me-clients.spec.ts +++ b/apps/services/auth/admin-api/src/app/v2/clients/test/me-clients.spec.ts @@ -7,10 +7,12 @@ import { AdminPatchClientDto, Client, clientBaseAttributes, + ClientDelegationType, ClientGrantType, defaultAcrValue, RefreshTokenExpiration, SequelizeConfigService, + SUPER_USER_DELEGATION_TYPES, translateRefreshTokenExpiration, } from '@island.is/auth-api-lib' import { User } from '@island.is/auth-nest-tools' @@ -51,6 +53,10 @@ const createTestClientData = async (app: TestApp, user: User) => { AuthDelegationType.LegalGuardian, AuthDelegationProvider.NationalRegistry, ], + [ + AuthDelegationType.LegalRepresentative, + AuthDelegationProvider.DistrictCommissionersRegistry, + ], ].map(async ([delegationType, provider]) => fixtureFactory.createDelegationType({ id: delegationType, @@ -620,6 +626,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, AuthDelegationType.LegalGuardian, + AuthDelegationType.LegalRepresentative, ], } @@ -636,6 +643,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, AuthDelegationType.LegalGuardian, + AuthDelegationType.LegalRepresentative, ]), ) }) @@ -986,6 +994,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, + AuthDelegationType.LegalRepresentative, ], } @@ -1000,6 +1009,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, + AuthDelegationType.LegalRepresentative, ], supportsCustomDelegation: true, supportsLegalGuardians: true, @@ -1028,6 +1038,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, + AuthDelegationType.LegalRepresentative, ], supportsCustomDelegation: true, supportsLegalGuardians: true, @@ -1041,6 +1052,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, + AuthDelegationType.LegalRepresentative, ], supportsCustomDelegation: true, supportsLegalGuardians: true, @@ -1054,6 +1066,7 @@ describe('MeClientsController with auth', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.PersonalRepresentative, AuthDelegationType.ProcurationHolder, + AuthDelegationType.LegalRepresentative, ], } @@ -1068,6 +1081,66 @@ describe('MeClientsController with auth', () => { supportsProcuringHolders: false, }) }) + + it.each` + action + ${'added'} + ${'removed'} + `( + 'should not have $action super user delegation type as normal', + async ({ action }) => { + // Arrange + const app = await setupApp({ + AppModule, + SequelizeConfigService, + user, + dbType: 'postgres', + }) + const server = request(app.getHttpServer()) + await createTestClientData(app, user) + + // Act + const res = await Promise.all( + SUPER_USER_DELEGATION_TYPES.map((delegationType) => + server + .patch( + `/v2/me/tenants/${tenantId}/clients/${encodeURIComponent( + clientId, + )}`, + ) + .send({ + [`${action}DelegationTypes`]: [delegationType], + }), + ), + ) + + // Assert + res.forEach((r) => { + expect(r.status).toEqual(403) + expect(r.body).toEqual({ + type: 'https://httpstatuses.org/403', + title: 'Forbidden', + status: 403, + detail: + 'User does not have access to update admin controlled fields.', + }) + }) + + // DB assert + const clientDelegationTypeModel = app.get( + getModelToken(ClientDelegationType), + ) + const clientDelegationTypes = await clientDelegationTypeModel.findAll( + { + where: { + clientId, + }, + }, + ) + + expect(clientDelegationTypes.length).toEqual(0) + }, + ) }) }) }) diff --git a/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts b/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts index d85cc5ba1808..89618911bb21 100644 --- a/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts +++ b/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts @@ -12,6 +12,7 @@ import { ApiScopeDelegationType, AdminPatchScopeDto, ApiScope, + SUPER_USER_DELEGATION_TYPES, } from '@island.is/auth-api-lib' import { FixtureFactory } from '@island.is/services/auth/testing' import { @@ -127,6 +128,10 @@ const createTestData = async ({ AuthDelegationType.LegalGuardian, AuthDelegationProvider.NationalRegistry, ], + [ + AuthDelegationType.LegalRepresentative, + AuthDelegationProvider.DistrictCommissionersRegistry, + ], ].map(async ([delegationType, provider]) => fixtureFactory.createDelegationType({ id: delegationType, @@ -374,6 +379,8 @@ interface PatchTestCase { allowExplicitDelegationGrant?: boolean grantToPersonalRepresentatives?: boolean isAccessControlled?: boolean + addedDelegationTypes?: AuthDelegationType[] + removedDelegationTypes?: AuthDelegationType[] } expected: { status: number @@ -513,6 +520,44 @@ const patchTestCases: Record = { }, } +const expected403Response = { + status: 403, + body: { + title: 'Forbidden', + status: 403, + detail: 'User does not have access to update admin controlled fields', + type: 'https://httpstatuses.org/403', + }, +} + +SUPER_USER_DELEGATION_TYPES.map((delegationType) => { + const delegationTypeName = AuthDelegationType[delegationType] + + patchTestCases[ + `should return a forbidden exception when adding super user delegation type: ${delegationTypeName}` + ] = { + user: currentUser, + tenantId: TENANT_ID, + scopeName: mockedPatchApiScope.name, + input: { + addedDelegationTypes: [delegationType], + }, + expected: expected403Response, + } + + patchTestCases[ + `should return a forbidden exception when removing super user delegation type: ${delegationTypeName}` + ] = { + user: currentUser, + tenantId: TENANT_ID, + scopeName: mockedPatchApiScope.name, + input: { + removedDelegationTypes: [delegationType], + }, + expected: expected403Response, + } +}) + describe('MeScopesController', () => { describe('with auth', () => { // GET: /v2/me/tenants/:tenantId/scopes @@ -761,7 +806,7 @@ describe('MeScopesController', () => { }) }) - describe('PATCH: /v2/me/tenants/:tenantId/scopes/:scopeName', () => { + describe('PATCH: /v2/me/tenants/:tenantId/scopes/:scopeName as super user', () => { let app: TestApp let server: request.SuperTest let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType @@ -831,6 +876,7 @@ describe('MeScopesController', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.LegalRepresentative, ], }, expected: { @@ -845,6 +891,7 @@ describe('MeScopesController', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.LegalRepresentative, ], }, }) @@ -858,6 +905,7 @@ describe('MeScopesController', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.LegalRepresentative, ], }, expected: { @@ -872,6 +920,7 @@ describe('MeScopesController', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.LegalRepresentative, ], }, }) @@ -883,6 +932,7 @@ describe('MeScopesController', () => { AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.LegalRepresentative, ], }, expected: { @@ -949,7 +999,7 @@ describe('MeScopesController', () => { }) }) - describe('POST: /v2/me/tenants/:tenantId/scopes', () => { + describe('POST: /v2/me/tenants/:tenantId/scopes as super user', () => { let app: TestApp let server: request.SuperTest let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType @@ -1035,7 +1085,7 @@ describe('MeScopesController', () => { }) }) - describe('POST: /v2/me/tenants/:tenantId/scopes', () => { + describe('POST: /v2/me/tenants/:tenantId/scopes as normal user', () => { let app: TestApp let server: request.SuperTest let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts index d97dc8d1e24f..d39fa234af6f 100644 --- a/libs/auth-api-lib/src/index.ts +++ b/libs/auth-api-lib/src/index.ts @@ -99,6 +99,7 @@ export * from './lib/resources/admin/dto/admin-create-scope.dto' export * from './lib/resources/admin/dto/admin-patch-scope.dto' export * from './lib/resources/resource-translation.service' export * from './lib/resources/scope.service' +export { SUPER_USER_DELEGATION_TYPES } from './lib/resources/utils/filters' // Clients module export * from './lib/clients/clients.module' diff --git a/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts b/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts index 0daa0665ddb2..443a1071149a 100644 --- a/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts +++ b/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts @@ -41,7 +41,10 @@ import { superUserFields, } from './dto/admin-patch-client.dto' import { ClientDelegationType } from '../models/client-delegation-type.model' -import { filterPersonalRepresentative } from '../../resources/utils/personalRepresentativeFilter' +import { + delegationTypeSuperUserFilter, + SUPER_USER_DELEGATION_TYPES, +} from '../../resources/utils/filters' export const clientBaseAttributes: Partial = { absoluteRefreshTokenLifetime: 8 * 60 * 60, // 8 hours @@ -179,7 +182,7 @@ export class AdminClientsService { // Remove defined super admin fields ...omit(clientDto, superUserFields), // Remove personal representative from delegation types since it is not allowed for non-super admins - supportedDelegationTypes: filterPersonalRepresentative( + supportedDelegationTypes: delegationTypeSuperUserFilter( clientDto.supportedDelegationTypes ?? [], ), } @@ -659,29 +662,25 @@ export class AdminClientsService { ) { const isSuperUser = this.isSuperAdmin(user) - const updatedFields = Object.keys(input) - const superUserUpdatedFields = updatedFields.filter((field) => - superUserFields.includes(field), - ) - - // Verify that the user is super admin, so they can update PersonalRepresentative in the delegation type + // Verify if superuser delegation types are being updated that user is super user const allDelegationTypes = [ ...(input.removedDelegationTypes ?? []), ...(input.addedDelegationTypes ?? []), ] - if (!isSuperUser && allDelegationTypes.length > 0) { - for (const delegationType of allDelegationTypes) { - if ( - delegationType.startsWith( - `${AuthDelegationType.PersonalRepresentative}:`, - ) - ) { - return false - } - } + const hasSuperUserDelegationType = allDelegationTypes.some( + (delegationType) => SUPER_USER_DELEGATION_TYPES.includes(delegationType), + ) + + if (!isSuperUser && hasSuperUserDelegationType) { + return false } + const updatedFields = Object.keys(input) + const superUserUpdatedFields = updatedFields.filter((field) => + superUserFields.includes(field), + ) + if (superUserUpdatedFields.length === 0) { // There are no superuser fields to update return true diff --git a/libs/auth-api-lib/src/lib/clients/admin/dto/admin-create-client.dto.ts b/libs/auth-api-lib/src/lib/clients/admin/dto/admin-create-client.dto.ts index 007872222c17..0d454c83115e 100644 --- a/libs/auth-api-lib/src/lib/clients/admin/dto/admin-create-client.dto.ts +++ b/libs/auth-api-lib/src/lib/clients/admin/dto/admin-create-client.dto.ts @@ -7,6 +7,8 @@ import { IsString, } from 'class-validator' +import { AuthDelegationType } from '@island.is/shared/types' + import { ClientType } from '../../../types' import { AdminPatchClientDto } from './admin-patch-client.dto' @@ -50,9 +52,10 @@ export class AdminCreateClientDto extends OmitType(AdminPatchClientDto, [ @IsArray() @IsOptional() + @IsEnum(AuthDelegationType, { each: true }) @ApiProperty({ example: ['Custom'], type: [String], }) - supportedDelegationTypes?: string[] + supportedDelegationTypes?: AuthDelegationType[] } diff --git a/libs/auth-api-lib/src/lib/clients/admin/dto/admin-patch-client.dto.ts b/libs/auth-api-lib/src/lib/clients/admin/dto/admin-patch-client.dto.ts index b94eb2776406..5cbb5b5758b5 100644 --- a/libs/auth-api-lib/src/lib/clients/admin/dto/admin-patch-client.dto.ts +++ b/libs/auth-api-lib/src/lib/clients/admin/dto/admin-patch-client.dto.ts @@ -12,6 +12,7 @@ import { import { TranslatedValueDto } from '../../../translation/dto/translated-value.dto' import { RefreshTokenExpiration } from '../../../types' import { AdminClientClaimDto } from './admin-client-claim.dto' +import { AuthDelegationType } from '@island.is/shared/types' export class AdminPatchClientDto { @ApiPropertyOptional({ @@ -72,14 +73,14 @@ export class AdminPatchClientDto { }) @IsOptional() @IsArray() - addedDelegationTypes?: string[] + addedDelegationTypes?: AuthDelegationType[] @ApiPropertyOptional({ description: 'Only super users can update this value.', }) @IsOptional() @IsArray() - removedDelegationTypes?: string[] + removedDelegationTypes?: AuthDelegationType[] @ApiPropertyOptional({ deprecated: true, diff --git a/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts b/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts index a24712f2fef5..4230c0f391a5 100644 --- a/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts +++ b/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts @@ -27,7 +27,10 @@ import { User } from '@island.is/auth-nest-tools' import { AdminPortalScope } from '@island.is/auth/scopes' import { AuthDelegationType } from '@island.is/shared/types' import { ApiScopeDelegationType } from '../models/api-scope-delegation-type.model' -import { filterPersonalRepresentative } from '../utils/personalRepresentativeFilter' +import { + delegationTypeSuperUserFilter, + SUPER_USER_DELEGATION_TYPES, +} from '../utils/filters' /** * This is a service that is used to access the admin scopes @@ -165,7 +168,7 @@ export class AdminScopeService { // Remove defined super admin fields ...omit(input, superUserScopeFields), // Remove personal representative from delegation types since it is not allowed for non-super admins - supportedDelegationTypes: filterPersonalRepresentative( + supportedDelegationTypes: delegationTypeSuperUserFilter( input.supportedDelegationTypes ?? [], ), } @@ -409,14 +412,11 @@ export class AdminScopeService { ...(input.removedDelegationTypes ?? []), ] - const isPersonalRepresentativeUpdate = allDelegationTypes.some( - (delegationType) => - delegationType.startsWith( - `${AuthDelegationType.PersonalRepresentative}:`, - ), + const hasSuperUserDelegationType = allDelegationTypes.some( + (delegationType) => SUPER_USER_DELEGATION_TYPES.includes(delegationType), ) - if (isPersonalRepresentativeUpdate && !isSuperUser) { + if (!isSuperUser && hasSuperUserDelegationType) { return false } diff --git a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts index fbc5f678fdb8..b0377207df9c 100644 --- a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts +++ b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger' -import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator' +import { + IsArray, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator' + +import { AuthDelegationType } from '@island.is/shared/types' import { AdminPatchScopeDto } from './admin-patch-scope.dto' @@ -14,9 +22,10 @@ export class AdminCreateScopeDto extends OmitType(AdminPatchScopeDto, [ @IsArray() @IsOptional() + @IsEnum(AuthDelegationType, { each: true }) @ApiPropertyOptional({ type: [String], example: ['Custom'], }) - supportedDelegationTypes?: string[] + supportedDelegationTypes?: AuthDelegationType[] } diff --git a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts index 374e31f454b2..11d4df948a9d 100644 --- a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts +++ b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts @@ -3,6 +3,7 @@ import { IsArray, IsBoolean, IsOptional, ValidateNested } from 'class-validator' import { Type } from 'class-transformer' import { TranslatedValueDto } from '../../../translation/dto/translated-value.dto' +import { AuthDelegationType } from '@island.is/shared/types' export class AdminPatchScopeDto { @ApiPropertyOptional({ @@ -95,7 +96,7 @@ export class AdminPatchScopeDto { type: [String], example: ['Custom'], }) - addedDelegationTypes?: string[] + addedDelegationTypes?: AuthDelegationType[] @IsArray() @IsOptional() @@ -103,7 +104,7 @@ export class AdminPatchScopeDto { type: [String], example: ['Custom'], }) - removedDelegationTypes?: string[] + removedDelegationTypes?: AuthDelegationType[] } /** diff --git a/libs/auth-api-lib/src/lib/resources/utils/filters.ts b/libs/auth-api-lib/src/lib/resources/utils/filters.ts new file mode 100644 index 000000000000..6b2bf4483156 --- /dev/null +++ b/libs/auth-api-lib/src/lib/resources/utils/filters.ts @@ -0,0 +1,19 @@ +import { AuthDelegationType } from '@island.is/shared/types' + +// Defined delegation types that are only manageable by superusers +export const SUPER_USER_DELEGATION_TYPES = [ + AuthDelegationType.PersonalRepresentative, + AuthDelegationType.LegalRepresentative, +] + +/** + * Filter and remove delegation types that are only authorized for superusers + * @param supportedDelegationType + */ +export const delegationTypeSuperUserFilter = ( + supportedDelegationType: AuthDelegationType[], +) => { + return supportedDelegationType.filter( + (delegationType) => !SUPER_USER_DELEGATION_TYPES.includes(delegationType), + ) +} diff --git a/libs/auth-api-lib/src/lib/resources/utils/personalRepresentativeFilter.ts b/libs/auth-api-lib/src/lib/resources/utils/personalRepresentativeFilter.ts deleted file mode 100644 index a76276d052b6..000000000000 --- a/libs/auth-api-lib/src/lib/resources/utils/personalRepresentativeFilter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AuthDelegationType } from '@island.is/shared/types' - -/** - * Filter out Personal Representative delegation type from the supported delegation types - * @param supportedDelegationType - */ -export const filterPersonalRepresentative = ( - supportedDelegationType: string[], -) => { - return supportedDelegationType.filter( - (delegationType) => - !delegationType.startsWith(AuthDelegationType.PersonalRepresentative), - ) -} diff --git a/libs/portals/admin/ids-admin/src/lib/messages.ts b/libs/portals/admin/ids-admin/src/lib/messages.ts index 3da23113e911..2b4e6a3f83d2 100644 --- a/libs/portals/admin/ids-admin/src/lib/messages.ts +++ b/libs/portals/admin/ids-admin/src/lib/messages.ts @@ -111,7 +111,7 @@ export const m = defineMessages({ defaultMessage: 'Name', }, displayNameDescription: { - id: 'ap.ids-admin:display-name', + id: 'ap.ids-admin:display-name-description', defaultMessage: 'Users see this when they sign in, and manage consents.', }, description: { @@ -920,16 +920,29 @@ export const m = defineMessages({ id: 'ap.ids-admin:client-delegation-type-personal-representative-postholf-name', defaultMessage: 'Documents (ísl. pósthólf)', }, + clientDelegationProviderSyslumennName: { + id: 'ap.ids-admin:client-delegation-provider-syslumenn-name', + defaultMessage: 'District Commissioner', + }, + clientDelegationProviderSyslumennDescription: { + id: 'ap.ids-admin:client-delegation-provider-syslumenn-description', + defaultMessage: + 'Allow users to sign into this application using delegation types managed by the District Commissioner.', + }, + clientDelegationTypeLegalRepresentativeName: { + id: 'ap.ids-admin:client-delegation-type-legal-representative-name', + defaultMessage: 'Legal representative', + }, apiScopeDelegationProviderDelegationdbName: { - id: 'ap.ids-admin:client-delegation-provider-custom-name', + id: 'ap.ids-admin:api-scope-delegation-provider-custom-name', defaultMessage: 'Island.is', }, apiScopeDelegationTypeCustomName: { - id: 'ap.ids-admin:client-delegation-type-custom-name', + id: 'ap.ids-admin:api-scope-delegation-type-custom-name', defaultMessage: 'Custom delegations', }, apiScopeDelegationTypeCustomDescription: { - id: 'ap.ids-admin:client-delegation-type-custom-description', + id: 'ap.ids-admin:api-scope-delegation-type-custom-description', defaultMessage: 'Should users be able to grant other users custom delegation for this permission.', }, @@ -970,6 +983,19 @@ export const m = defineMessages({ apiScopeDelegationTypePersonalRepresentativepostholfDescription: { id: 'ap.ids-admin:api-scope-delegation-type-personal-representative-postholf-description', defaultMessage: - 'Should personal representatives automatically get this permission for their clients', + 'Should personal representatives automatically get this permission for their clients.', + }, + apiScopeDelegationProviderSyslumennName: { + id: 'ap.ids-admin:api-scope-delegation-provider-syslumenn-name', + defaultMessage: 'District Commissioner', + }, + apiScopeDelegationTypeLegalRepresentativeName: { + id: 'ap.ids-admin:api-scope-delegation-type-legal-representative-name', + defaultMessage: 'Legal representative', + }, + apiScopeDelegationTypeLegalRepresentativeDescription: { + id: 'ap.ids-admin:api-scope-delegation-type-legal-representative-description', + defaultMessage: + 'Should legal representative automatically get this permission for their clients.', }, }) diff --git a/libs/portals/admin/ids-admin/src/screens/Client/components/Delegation.tsx b/libs/portals/admin/ids-admin/src/screens/Client/components/Delegation.tsx index 09eb4c31b003..8cb2f48b3573 100644 --- a/libs/portals/admin/ids-admin/src/screens/Client/components/Delegation.tsx +++ b/libs/portals/admin/ids-admin/src/screens/Client/components/Delegation.tsx @@ -131,8 +131,10 @@ const Delegation = ({ {providers.map((provider) => !provider || (!isSuperAdmin && - provider.id === - AuthDelegationProvider.PersonalRepresentativeRegistry) ? null : ( + (provider.id === + AuthDelegationProvider.PersonalRepresentativeRegistry || + provider.id === + AuthDelegationProvider.DistrictCommissionersRegistry)) ? null : (
diff --git a/libs/portals/admin/ids-admin/src/screens/Permission/components/PermissionDelegations.tsx b/libs/portals/admin/ids-admin/src/screens/Permission/components/PermissionDelegations.tsx index 822121e00696..d60a688358dc 100644 --- a/libs/portals/admin/ids-admin/src/screens/Permission/components/PermissionDelegations.tsx +++ b/libs/portals/admin/ids-admin/src/screens/Permission/components/PermissionDelegations.tsx @@ -118,8 +118,10 @@ export const PermissionDelegations = () => { {providers.map((provider) => !provider || (!isSuperAdmin && - provider.id === - AuthDelegationProvider.PersonalRepresentativeRegistry) ? null : ( + (provider.id === + AuthDelegationProvider.PersonalRepresentativeRegistry || + provider.id === + AuthDelegationProvider.DistrictCommissionersRegistry)) ? null : (
From 5de8553b9a5aef05f69f5662311be373ab5a3131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorkell=20M=C3=A1ni=20=C3=9Eorkelsson?= Date: Thu, 19 Sep 2024 13:35:09 +0000 Subject: [PATCH 085/173] feat(service-portal): Revert modal upgrade (#16080) This reverts commit 6bfcab321bfc8e93b13127c3a6e92b057f70fd90. --- .../core/src/components/Modal/Modal.css.ts | 10 -- .../core/src/components/Modal/Modal.tsx | 86 +-------------- .../RegisterModal/RegisterModal.css.ts | 37 +++++++ .../RegisterModal/RegisterModal.tsx | 99 +++++++++++++++++ .../src/components/RegisterModal/index.ts | 1 + .../DentistRegistration.tsx | 102 +++++++----------- .../HealthCenterRegistration.tsx | 84 ++++++--------- 7 files changed, 208 insertions(+), 211 deletions(-) create mode 100644 libs/service-portal/health/src/components/RegisterModal/RegisterModal.css.ts create mode 100644 libs/service-portal/health/src/components/RegisterModal/RegisterModal.tsx create mode 100644 libs/service-portal/health/src/components/RegisterModal/index.ts diff --git a/libs/service-portal/core/src/components/Modal/Modal.css.ts b/libs/service-portal/core/src/components/Modal/Modal.css.ts index a471a7f93337..95d03a4cef5a 100644 --- a/libs/service-portal/core/src/components/Modal/Modal.css.ts +++ b/libs/service-portal/core/src/components/Modal/Modal.css.ts @@ -27,13 +27,3 @@ export const closeButton = style({ right: theme.spacing['1'], zIndex: 2, }) - -export const image = style({ - display: 'none', - ...themeUtils.responsiveStyle({ - lg: { - marginRight: `-${theme.spacing['2']}px`, - display: 'initial', - }, - }), -}) diff --git a/libs/service-portal/core/src/components/Modal/Modal.tsx b/libs/service-portal/core/src/components/Modal/Modal.tsx index d668ac6f0409..51720105b1bb 100644 --- a/libs/service-portal/core/src/components/Modal/Modal.tsx +++ b/libs/service-portal/core/src/components/Modal/Modal.tsx @@ -1,14 +1,6 @@ -import React, { FC, ReactElement, useEffect, useState } from 'react' +import React, { FC, ReactElement } from 'react' import * as styles from './Modal.css' -import { - Box, - Text, - ModalBase, - Button, - ButtonProps, - Inline, -} from '@island.is/island-ui/core' -import { useDebounce } from 'react-use' +import { Box, ModalBase, Button } from '@island.is/island-ui/core' interface Props { id: string @@ -18,17 +10,6 @@ interface Props { initialVisibility?: boolean disclosure?: ReactElement label?: string - title?: string - text?: string - buttons?: Array<{ - id: ButtonProps['id'] - type?: 'ghost' | 'primary' | 'utility' - onClick?: () => void - text?: string - loading?: boolean - }> - iconSrc?: string - iconAlt?: string /** * No styling. All callbacks available. */ @@ -43,39 +24,12 @@ export const Modal: FC> = ({ disclosure, isVisible, label, - title, - text, - buttons, initialVisibility = true, skeleton, - iconAlt, - iconSrc, }) => { - const [closing, setClosing] = useState(false) - const [startClosing, setStartClosing] = useState(false) - - useEffect(() => { - if (closing) { - onCloseModal && onCloseModal() - setClosing(false) - setStartClosing(false) - } - }, [closing, onCloseModal]) - - useDebounce( - () => { - if (startClosing) { - setClosing(startClosing) - } - }, - 500, - [startClosing], - ) - const handleOnVisibilityChange = (isVisible: boolean) => { - !isVisible && onCloseModal && setStartClosing(true) + !isVisible && onCloseModal && onCloseModal() } - return ( > = ({ ) : ( @@ -111,37 +61,7 @@ export const Modal: FC> = ({ size="large" /> - - - {title && ( - - {title} - - )} - {text && {text}} - - {buttons && ( - - {buttons.map((b) => ( - - ))} - - )} - {children} - {iconSrc && ( - - {iconAlt} - - )} ) } diff --git a/libs/service-portal/health/src/components/RegisterModal/RegisterModal.css.ts b/libs/service-portal/health/src/components/RegisterModal/RegisterModal.css.ts new file mode 100644 index 000000000000..72713fc6fe52 --- /dev/null +++ b/libs/service-portal/health/src/components/RegisterModal/RegisterModal.css.ts @@ -0,0 +1,37 @@ +import { style } from '@vanilla-extract/css' + +export const modalBaseStyle = style({ + maxWidth: '55.5rem', + position: 'absolute', + width: 'calc(100% - 2rem)', + inset: '1rem', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', +}) + +export const modalGridStyle = style({ + display: 'grid', + gridTemplateColumns: '1fr 4fr 3fr', +}) + +export const modalGridContentStyle = style({ + gridColumnStart: 2, +}) + +export const modalGridImageStyle = style({ + placeSelf: 'center', +}) + +export const closeModalButtonStyle = style({ + position: 'absolute', + top: '1rem', + right: '1rem', + cursor: 'pointer', +}) + +export const modalGridButtonGroup = style({ + display: 'flex', + flexDirection: 'row', + gap: '1.5rem', +}) diff --git a/libs/service-portal/health/src/components/RegisterModal/RegisterModal.tsx b/libs/service-portal/health/src/components/RegisterModal/RegisterModal.tsx new file mode 100644 index 000000000000..23b5f589dcbe --- /dev/null +++ b/libs/service-portal/health/src/components/RegisterModal/RegisterModal.tsx @@ -0,0 +1,99 @@ +import { + ModalBase, + Icon, + Box, + Button, + Text, + Select, +} from '@island.is/island-ui/core' +import { messages } from '../../lib/messages' +import * as styles from './RegisterModal.css' +import { useLocale } from '@island.is/localization' +import { useState } from 'react' +import { HealthCenterDoctorOption } from '../../screens/HealthCenterRegistration/HealthCenterRegistration' + +type RegisterModalProps = { + onClose: () => void + onAccept: (doctorId?: number) => void + id: string + title: string + description: string + buttonLoading?: boolean + isVisible?: boolean + healthCenterDoctors?: HealthCenterDoctorOption[] +} + +export const RegisterModal = ({ + id, + onAccept, + onClose, + title, + description, + buttonLoading = false, + isVisible = false, + healthCenterDoctors, +}: RegisterModalProps) => { + const { formatMessage } = useLocale() + const [doctorId, setDoctorId] = useState() + + return ( + + + + + + + + {title} + {description ? ( + + {description} + + ) : ( + // Temp fix - will refactor and use core model component + + )} + {healthCenterDoctors?.length ? ( + + + + ) +} + +export default InputNationalId diff --git a/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx index 6fff397b7f80..ab612e15b6c0 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/components/DefendantInfo/DefendantInfo.tsx @@ -1,5 +1,4 @@ import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' -import InputMask from 'react-input-mask' import { useIntl } from 'react-intl' import { @@ -17,6 +16,7 @@ import { isIndictmentCase } from '@island.is/judicial-system/types' import { core } from '@island.is/judicial-system-web/messages' import { BlueBox } from '@island.is/judicial-system-web/src/components' import InputName from '@island.is/judicial-system-web/src/components/Inputs/InputName' +import InputNationalId from '@island.is/judicial-system-web/src/components/Inputs/InputNationalId' import { Defendant, Gender, @@ -68,8 +68,6 @@ const DefendantInfo: FC = (props) => { { label: formatMessage(core.otherGender), value: Gender.OTHER }, ] - const [nationalIdErrorMessage, setNationalIdErrorMessage] = - useState('') const [nationalIdNotFound, setNationalIdNotFound] = useState(false) const [accusedAddressErrorMessage, setAccusedAddressErrorMessage] = @@ -96,7 +94,7 @@ const DefendantInfo: FC = (props) => { if (personData && personData.items && personData.items.length > 0) { setAccusedAddressErrorMessage('') - setNationalIdErrorMessage('') + setNationalIdNotFound(false) setIsGenderAndCitizenshipDisabled(false) onChange({ @@ -119,7 +117,6 @@ const DefendantInfo: FC = (props) => { if (businessData && businessData.items && businessData.items.length > 0) { setAccusedAddressErrorMessage('') - setNationalIdErrorMessage('') setIsGenderAndCitizenshipDisabled(true) onChange({ @@ -162,14 +159,13 @@ const DefendantInfo: FC = (props) => { checked={Boolean(defendant.noNationalId)} onChange={() => { setNationalIdNotFound(false) - setNationalIdErrorMessage('') updateDefendantState( { caseId: workingCase.id, defendantId: defendant.id, noNationalId: !defendant.noNationalId, - nationalId: undefined, + nationalId: null, }, setWorkingCase, ) @@ -178,7 +174,7 @@ const DefendantInfo: FC = (props) => { caseId: workingCase.id, defendantId: defendant.id, noNationalId: !defendant.noNationalId, - nationalId: undefined, + nationalId: null, }) }} filled @@ -187,65 +183,29 @@ const DefendantInfo: FC = (props) => { /> - { - setNationalIdNotFound(false) - removeErrorMessageIfValid( - defendant.noNationalId - ? ['date-of-birth'] - : ['empty', 'national-id'], - evt.target.value, - nationalIdErrorMessage, - setNationalIdErrorMessage, - ) - + onBlur={(value) => + onChange({ + caseId: workingCase.id, + defendantId: defendant.id, + nationalId: value, + }) + } + onChange={(value) => updateDefendantState( { caseId: workingCase.id, defendantId: defendant.id, - nationalId: evt.target.value, + nationalId: value, }, setWorkingCase, ) - }} - onBlur={async (evt) => { - validateAndSetErrorMessage( - defendant.noNationalId - ? ['date-of-birth'] - : ['empty', 'national-id'], - evt.target.value, - setNationalIdErrorMessage, - ) - - onChange({ - caseId: workingCase.id, - defendantId: defendant.id, - nationalId: evt.target.value, - }) - }} + } disabled={nationalIdImmutable} - > - - + required={!defendant.noNationalId} + /> {defendant.nationalId?.length === 11 && nationalIdNotFound && ( {formatMessage(core.nationalIdNotFoundInNationalRegistry)} From 4d53ae2842386c317173a2115c291c2b677e7f4d Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:11:45 +0000 Subject: [PATCH 092/173] feat(web): Add default header for samgongustofa organization (#16060) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../components/Organization/Wrapper/OrganizationWrapper.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 2602ea233cd1..6a312f36eacd 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -471,7 +471,9 @@ export const OrganizationHeader: React.FC< /> ) case 'samgongustofa': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Thu, 19 Sep 2024 16:26:09 +0000 Subject: [PATCH 093/173] feat(web): Add default header for rikissaksoknari organization (#16095) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Wrapper/OrganizationWrapper.css.ts | 36 +++++++++++++++++++ .../Wrapper/OrganizationWrapper.tsx | 11 +++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts index 607a14ce626c..5247afc5c39c 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts @@ -57,6 +57,7 @@ export const shhHeaderGridContainerBase = style({ }, }), }) + export const shhHeaderGridContainerWidth = style([ shhHeaderGridContainerBase, themeUtils.responsiveStyle({ @@ -67,4 +68,39 @@ export const shhHeaderGridContainerWidth = style([ }, }), ]) + export const shhHeaderGridContainerWidthSubpage = shhHeaderGridContainerBase + +export const rikissaksoknariHeaderGridContainerBase = style({ + display: 'grid', + maxWidth: '1342px', + margin: '0 auto', + backgroundBlendMode: 'saturation', + backgroundRepeat: 'no-repeat', + background: ` + linear-gradient(180deg, #000000 0%, rgba(0, 0, 0, 0) 92.19%), + linear-gradient(-93.41deg, rgba(0, 72, 153, 0.4) -4.61%, rgba(0, 72, 153, 0.62) 17.93%, rgba(0, 72, 153, 0.82) 47.83%, rgba(0, 72, 153, 0.94) 71.31%, rgba(0, 72, 153, 0.95) 94.11%, #004899 114.58%) + `, + ...themeUtils.responsiveStyle({ + lg: { + gridTemplateRows: '315px', + gridTemplateColumns: '65fr 35fr', + }, + }), +}) + +export const rikissaksoknariHeaderGridContainerWidth = style([ + rikissaksoknariHeaderGridContainerBase, + themeUtils.responsiveStyle({ + lg: { + background: ` + url('https://images.ctfassets.net/8k0h54kbe6bj/3k02pUiq44p3Hn6eLf2VuQ/23bbe8981afa02668ddd522e3dc6988f/rikissaksoknari-mynd_1.png') no-repeat right, + linear-gradient(180deg, #000000 0%, rgba(0, 0, 0, 0) 92.19%), + linear-gradient(-93.41deg, rgba(0, 72, 153, 0.4) -4.61%, rgba(0, 72, 153, 0.62) 17.93%, rgba(0, 72, 153, 0.82) 47.83%, rgba(0, 72, 153, 0.94) 71.31%, rgba(0, 72, 153, 0.95) 94.11%, #004899 114.58%) + `, + }, + }), +]) + +export const rikissaksoknariHeaderGridContainerSubpage = + rikissaksoknariHeaderGridContainerBase diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 6a312f36eacd..0ee977d92b69 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -518,7 +518,16 @@ export const OrganizationHeader: React.FC< /> ) case 'rikissaksoknari': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Thu, 19 Sep 2024 17:26:37 +0000 Subject: [PATCH 094/173] feat(native-app): update applications screen (#15964) * feat: first version of updating applications * feat: first version of applications subscreens * feat: reusable component for applications list * feat: new progress bar and final ui for status card * fix: change unfinished to incomplete and finished to completed to keep coordination * feat: add applications preview component to use on applications screen * feat: applications module now uses applications preview as well * fix: add bottom tabs indicator on applications screen * feat: finalizing progress meter * fix: getTypeAndBadgeVariant should match sorting * fix: loading state for list screens * fix: add margin to skeleton loaders for lists * fix: add error to applications list of something goes wrong and no data in cache * feat: add pull to refresh on new applications page * fix: fix imports * fix: revert changes on view around empty state for applications * fix: remove unused stuff from graphql query * fix: stop checking for state and prerequisites for incomplete applications * fix: address nit comments on PR * fix: make sure to not send new Date into new Date * fix: rename props in progress meter * feat: use createSkeletonArr helper function * fix: add px to margin-bottom * fix: include not started applications in incomplete applications * feat: use tags and pendingAction from server if provided * fix: send locale to applications query * fix: only query for draft not notstarted applications until backed is fixed * fix: 3 loader items instead of 5 --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../fragments/application.fragment.graphql | 12 + .../src/graphql/queries/applications.graphql | 4 +- apps/native/app/src/messages/en.ts | 27 ++- apps/native/app/src/messages/is.ts | 32 ++- .../applications/applications-completed.tsx | 68 ++++++ .../applications/applications-in-progress.tsx | 64 +++++ .../applications/applications-incomplete.tsx | 64 +++++ .../src/screens/applications/applications.tsx | 216 ++++++++--------- .../components/applications-list.tsx | 225 ++++++++++++++++++ .../components/applications-preview.tsx | 168 +++++++++++++ .../applications/utils/getApplicationType.ts | 21 ++ .../applications/utils/getBadgeVariant.ts | 21 ++ .../src/screens/home/applications-module.tsx | 132 ++-------- apps/native/app/src/ui/index.ts | 1 + apps/native/app/src/ui/lib/badge/badge.tsx | 86 +++++-- .../app/src/ui/lib/card/status-card.tsx | 125 +++++----- .../src/ui/lib/problem/problem-template.tsx | 16 +- .../ui/lib/progress-meter/progress-meter.tsx | 104 ++++++++ .../app/src/utils/component-registry.ts | 3 + .../src/utils/lifecycle/setup-components.tsx | 12 + .../app/src/utils/lifecycle/setup-routes.ts | 38 ++- 21 files changed, 1082 insertions(+), 357 deletions(-) create mode 100644 apps/native/app/src/screens/applications/applications-completed.tsx create mode 100644 apps/native/app/src/screens/applications/applications-in-progress.tsx create mode 100644 apps/native/app/src/screens/applications/applications-incomplete.tsx create mode 100644 apps/native/app/src/screens/applications/components/applications-list.tsx create mode 100644 apps/native/app/src/screens/applications/components/applications-preview.tsx create mode 100644 apps/native/app/src/screens/applications/utils/getApplicationType.ts create mode 100644 apps/native/app/src/screens/applications/utils/getBadgeVariant.ts create mode 100644 apps/native/app/src/ui/lib/progress-meter/progress-meter.tsx diff --git a/apps/native/app/src/graphql/fragments/application.fragment.graphql b/apps/native/app/src/graphql/fragments/application.fragment.graphql index cb64abe20ce5..60b7e4498bdc 100644 --- a/apps/native/app/src/graphql/fragments/application.fragment.graphql +++ b/apps/native/app/src/graphql/fragments/application.fragment.graphql @@ -8,4 +8,16 @@ fragment ApplicationFragment on Application { name progress status + institution + actionCard { + draftTotalSteps + draftFinishedSteps + tag { + label + variant + } + pendingAction { + title + } + } } diff --git a/apps/native/app/src/graphql/queries/applications.graphql b/apps/native/app/src/graphql/queries/applications.graphql index 6fc826e99730..15bde50f3100 100644 --- a/apps/native/app/src/graphql/queries/applications.graphql +++ b/apps/native/app/src/graphql/queries/applications.graphql @@ -1,5 +1,5 @@ -query ListApplications { - applicationApplications { +query ListApplications($input: ApplicationApplicationsInput, $locale: String) { + applicationApplications(input: $input, locale: $locale) { ...ApplicationFragment } } diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 5dbbb6010dc4..0662636e4777 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -474,27 +474,34 @@ export const en: TranslatedMessages = { // applications screen 'applications.title': 'Applications', 'applications.bottomTabText': 'Applications', - 'applications.searchPlaceholder': 'Search...', - 'applications.loadingText': 'Searching...', - 'applications.resultText': 'results', - 'applications.singleResultText': 'result', - 'applications.noResultText': 'No results', - 'applications.emptyListTitle': 'There are currently no links', - 'applications.emptyListDescription': - 'There are no links available at the moment', + 'applications.emptyTitle': 'No applications', + 'applications.emptyDescription': + 'When you apply for services from the government, they will appear here.', + 'applications.incomplete': 'Unfinished applications', + 'applications.completed': 'Finished applications', + 'applications.inProgress': 'Applications in progress', // cards 'applicationStatusCard.openButtonLabel': 'Open application', - 'applicationStatusCard.seeMoreApplications': 'View applications', + 'applicationStatusCard.description': `{ + state, + select, + inprogress {The application is being processed} + completed {Completed} + rejected {Rejected} + other {} + }`, 'applicationStatusCard.status': `{ state, select, inprogress {In progress} completed {Completed} rejected {Rejected} + draft {Application in progress} other {Unknown status} }`, - 'applicationStatusCard.noActiveApplications': 'No active applications', + 'applicationStatusCard.draftProgress': + 'You have completed {draftFinishedSteps} of {draftTotalSteps} steps', // edit phone 'edit.phone.screenTitle': 'Edit Phone', diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index ac89579c2ed5..19e80cf23b38 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -473,28 +473,34 @@ export const is = { // applications screen 'applications.title': 'Umsóknir', 'applications.bottomTabText': 'Umsóknir', - 'applications.searchPlaceholder': 'Leita að umsóknum...', - 'applications.loadingText': 'Leita í skjölum...', - 'applications.resultText': 'niðurstöður fundust', - 'applications.singleResultText': 'niðurstaða fannst', - 'applications.noResultText': 'Engar niðurstöður fundust', - 'applications.emptyListTitle': 'Hér eru engir linkar sem stendur', - 'applications.emptyListDescription': - 'Að einhverjum ástæðum þá eru engir linkar að umsóknum aðgengilegir eins og er', + 'applications.emptyTitle': 'Engar umsóknir', + 'applications.emptyDescription': + 'Þegar þú stofnar stafræna umsókn á Ísland.is birtist staða hennar hér.', + 'applications.incomplete': 'Ókláraðar umsóknir', + 'applications.completed': 'Afgreiddar umsóknir', + 'applications.inProgress': 'Umsóknir í vinnslu', // cards 'applicationStatusCard.openButtonLabel': 'Opna umsókn', - 'applicationStatusCard.seeMoreApplications': 'Skoða umsóknir', + 'applicationStatusCard.description': `{ + state, + select, + inprogress {Umsókn er í afgreiðsluferli} + completed {Samþykkt} + rejected {Hafnað} + other {} + }`, 'applicationStatusCard.status': `{ state, select, - inprogress {Í ferli} - completed {Lokið} + inprogress {Í vinnslu} + completed {Afgreidd} rejected {Hafnað} + draft {Í vinnslu hjá þér} other {Staða óþekkt} }`, - 'applicationStatusCard.noActiveApplications': - 'Þegar þú stofnar stafræna umsókn á Ísland.is birtist staða hennar hér.', + 'applicationStatusCard.draftProgress': + 'Þú hefur klárað {draftFinishedSteps} af {draftTotalSteps} skrefum', // edit phone 'edit.phone.screenTitle': 'Breyta símanúmeri', diff --git a/apps/native/app/src/screens/applications/applications-completed.tsx b/apps/native/app/src/screens/applications/applications-completed.tsx new file mode 100644 index 000000000000..1038c67b87f3 --- /dev/null +++ b/apps/native/app/src/screens/applications/applications-completed.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react' +import { NavigationFunctionComponent } from 'react-native-navigation' + +import { + ApplicationResponseDtoStatusEnum, + useListApplicationsQuery, +} from '../../graphql/types/schema' +import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' +import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' +import { ApplicationsList } from './components/applications-list' +import { usePreferencesStore } from '../../stores/preferences-store' + +const { useNavigationOptions, getNavigationOptions } = + createNavigationOptionHooks( + (theme, intl) => ({ + topBar: { + title: { + text: intl.formatMessage({ id: 'applications.completed' }), + }, + noBorder: true, + }, + }), + { + bottomTabs: { + visible: false, + drawBehind: true, + }, + }, + ) + +export const ApplicationsCompletedScreen: NavigationFunctionComponent = ({ + componentId, +}) => { + useNavigationOptions(componentId) + const [refetching, setRefetching] = useState(false) + const { locale } = usePreferencesStore() + + const applicationsRes = useListApplicationsQuery({ + variables: { + input: { + status: [ + ApplicationResponseDtoStatusEnum.Completed, + ApplicationResponseDtoStatusEnum.Rejected, + ApplicationResponseDtoStatusEnum.Approved, + ], + }, + locale: locale === 'is-US' ? 'is' : 'en', + }, + }) + + useConnectivityIndicator({ + componentId, + refetching, + queryResult: applicationsRes, + }) + + return ( + + ) +} + +ApplicationsCompletedScreen.options = getNavigationOptions diff --git a/apps/native/app/src/screens/applications/applications-in-progress.tsx b/apps/native/app/src/screens/applications/applications-in-progress.tsx new file mode 100644 index 000000000000..08c1c7f3d47e --- /dev/null +++ b/apps/native/app/src/screens/applications/applications-in-progress.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import { NavigationFunctionComponent } from 'react-native-navigation' + +import { + ApplicationResponseDtoStatusEnum, + useListApplicationsQuery, +} from '../../graphql/types/schema' +import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' +import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' +import { ApplicationsList } from './components/applications-list' +import { usePreferencesStore } from '../../stores/preferences-store' + +const { useNavigationOptions, getNavigationOptions } = + createNavigationOptionHooks( + (theme, intl) => ({ + topBar: { + title: { + text: intl.formatMessage({ id: 'applications.inProgress' }), + }, + noBorder: true, + }, + }), + { + bottomTabs: { + visible: false, + drawBehind: true, + }, + }, + ) + +export const ApplicationsInProgressScreen: NavigationFunctionComponent = ({ + componentId, +}) => { + useNavigationOptions(componentId) + const [refetching, setRefetching] = useState(false) + const { locale } = usePreferencesStore() + + const applicationsRes = useListApplicationsQuery({ + variables: { + input: { + status: [ApplicationResponseDtoStatusEnum.Inprogress], + }, + locale: locale === 'is-US' ? 'is' : 'en', + }, + }) + + useConnectivityIndicator({ + componentId, + refetching, + queryResult: applicationsRes, + }) + + return ( + + ) +} + +ApplicationsInProgressScreen.options = getNavigationOptions diff --git a/apps/native/app/src/screens/applications/applications-incomplete.tsx b/apps/native/app/src/screens/applications/applications-incomplete.tsx new file mode 100644 index 000000000000..70874676aed1 --- /dev/null +++ b/apps/native/app/src/screens/applications/applications-incomplete.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import { NavigationFunctionComponent } from 'react-native-navigation' + +import { + ApplicationResponseDtoStatusEnum, + useListApplicationsQuery, +} from '../../graphql/types/schema' +import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' +import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' +import { ApplicationsList } from './components/applications-list' +import { usePreferencesStore } from '../../stores/preferences-store' + +const { useNavigationOptions, getNavigationOptions } = + createNavigationOptionHooks( + (theme, intl) => ({ + topBar: { + title: { + text: intl.formatMessage({ id: 'applications.incomplete' }), + }, + noBorder: true, + }, + }), + { + bottomTabs: { + visible: false, + drawBehind: true, + }, + }, + ) + +export const ApplicationsIncompleteScreen: NavigationFunctionComponent = ({ + componentId, +}) => { + useNavigationOptions(componentId) + const [refetching, setRefetching] = useState(false) + const { locale } = usePreferencesStore() + + const applicationsRes = useListApplicationsQuery({ + variables: { + input: { + status: [ApplicationResponseDtoStatusEnum.Draft], + }, + locale: locale === 'is-US' ? 'is' : 'en', + }, + }) + + useConnectivityIndicator({ + componentId, + refetching, + queryResult: applicationsRes, + }) + + return ( + + ) +} + +ApplicationsIncompleteScreen.options = getNavigationOptions diff --git a/apps/native/app/src/screens/applications/applications.tsx b/apps/native/app/src/screens/applications/applications.tsx index fe5117c18e28..33b459f40811 100644 --- a/apps/native/app/src/screens/applications/applications.tsx +++ b/apps/native/app/src/screens/applications/applications.tsx @@ -1,36 +1,24 @@ -import { EmptyList, Heading, ListButton, TopLine } from '@ui' -import { useCallback, useMemo, useRef, useState } from 'react' +import { EmptyList, StatusCardSkeleton } from '@ui' +import { useCallback, useMemo, useState } from 'react' import { useIntl } from 'react-intl' -import { - Animated, - FlatList, - Image, - RefreshControl, - SafeAreaView, - View, -} from 'react-native' +import { Image, RefreshControl, ScrollView, View } from 'react-native' import { NavigationFunctionComponent } from 'react-native-navigation' import { useNavigationComponentDidAppear } from 'react-native-navigation-hooks' -import illustrationSrc from '../../assets/illustrations/le-company-s3.png' -import { BottomTabsIndicator } from '../../components/bottom-tabs-indicator/bottom-tabs-indicator' +import { useTheme } from 'styled-components' + +import illustrationSrc from '../../assets/illustrations/le-jobs-s3.png' import { Application, - SearchArticleFragmentFragment, - SearchableContentTypes, + ApplicationResponseDtoStatusEnum, useListApplicationsQuery, - useListSearchQuery, } from '../../graphql/types/schema' import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' -import { useBrowser } from '../../lib/use-browser' -import { getApplicationOverviewUrl } from '../../utils/applications-utils' import { testIDs } from '../../utils/test-ids' -import { ApplicationsModule } from '../home/applications-module' import { isIos } from '../../utils/devices' - -type ListItem = - | { id: string; __typename: 'Skeleton' } - | SearchArticleFragmentFragment +import { ApplicationsPreview } from './components/applications-preview' +import { BottomTabsIndicator } from '../../components/bottom-tabs-indicator/bottom-tabs-indicator' +import { usePreferencesStore } from '../../stores/preferences-store' const { useNavigationOptions, getNavigationOptions } = createNavigationOptionHooks( @@ -71,84 +59,80 @@ const { useNavigationOptions, getNavigationOptions } = }, ) +interface SortedApplication { + incomplete: Application[] + inProgress: Application[] + completed: Application[] +} + +export const sortApplicationsStatus = ( + applications: Application[], +): SortedApplication => { + const incomplete: Application[] = [] + const inProgress: Application[] = [] + const completed: Application[] = [] + + applications.forEach((application) => { + if ( + application.status === ApplicationResponseDtoStatusEnum.Draft || + application.status === ApplicationResponseDtoStatusEnum.Notstarted + ) { + incomplete.push(application) + } else if ( + application.status === ApplicationResponseDtoStatusEnum.Inprogress + ) { + inProgress.push(application) + } else { + completed.push(application) + } + }) + + return { + incomplete, + inProgress, + completed, + } +} + export const ApplicationsScreen: NavigationFunctionComponent = ({ componentId, }) => { useNavigationOptions(componentId) - const { openBrowser } = useBrowser() - const flatListRef = useRef(null) - const [refetching, setRefetching] = useState(false) const intl = useIntl() - const scrollY = useRef(new Animated.Value(0)).current + const theme = useTheme() + const [refetching, setRefetching] = useState(false) const [hiddenContent, setHiddenContent] = useState(isIos) + const { locale } = usePreferencesStore() - const res = useListSearchQuery({ - variables: { - input: { - queryString: '*', - types: [SearchableContentTypes.WebArticle], - contentfulTags: ['umsokn'], - size: 100, - page: 1, - }, - }, + const applicationsRes = useListApplicationsQuery({ + variables: { locale: locale === 'is-US' ? 'is' : 'en' }, }) - const applicationsRes = useListApplicationsQuery() + const applications = useMemo( + () => applicationsRes.data?.applicationApplications ?? [], + [applicationsRes], + ) useConnectivityIndicator({ componentId, refetching, - queryResult: [applicationsRes, res], + queryResult: applicationsRes, }) + const sortedApplications = useMemo( + () => sortApplicationsStatus(applications as Application[]), + [applications], + ) + useNavigationComponentDidAppear(() => { setHiddenContent(false) }, componentId) - const data = useMemo(() => { - if (!res.data && res.loading) { - return Array.from({ length: 8 }).map((_, id) => ({ - __typename: 'Skeleton', - id: id.toString(), - })) - } - - const articles = [ - ...(res?.data?.searchResults?.items ?? []), - ] as SearchArticleFragmentFragment[] - - return articles.sort((a, b) => a.title.localeCompare(b.title)) - }, [res.data, res.loading]) - - const renderItem = useCallback( - ({ item }: { item: ListItem; index: number }) => { - if (item.__typename === 'Skeleton') { - return - } - if (item.__typename === 'Article') { - return ( - - openBrowser(getApplicationOverviewUrl(item), componentId) - } - /> - ) - } - return null - }, - [], - ) - - const keyExtractor = useCallback((item: ListItem) => item.id, []) - const onRefresh = useCallback(async () => { setRefetching(true) try { - await res.refetch() + await applicationsRes.refetch() } catch (e) { // noop } finally { @@ -163,58 +147,56 @@ export const ApplicationsScreen: NavigationFunctionComponent = ({ return ( <> - + } + > + {!applications.length && !applicationsRes.loading ? ( } /> - } - renderItem={renderItem} - ListHeaderComponent={ - - - - - {intl.formatMessage({ id: 'home.allApplications' })} - - - - } - refreshControl={ - - } - /> + ) : null} + {applicationsRes.loading && + !applicationsRes.data && + Array.from({ length: 3 }).map((_, index) => ( + + + + ))} + + + + - ) } diff --git a/apps/native/app/src/screens/applications/components/applications-list.tsx b/apps/native/app/src/screens/applications/components/applications-list.tsx new file mode 100644 index 000000000000..4d6234a1777b --- /dev/null +++ b/apps/native/app/src/screens/applications/components/applications-list.tsx @@ -0,0 +1,225 @@ +import { + Badge, + badgeColorSchemes, + EmptyList, + Problem, + StatusCard, + StatusCardSkeleton, + TopLine, +} from '@ui' +import { useCallback, useMemo, useRef, useState } from 'react' +import { useIntl } from 'react-intl' +import { + Animated, + FlatList, + Image, + ListRenderItemInfo, + RefreshControl, + View, +} from 'react-native' +import { useTheme } from 'styled-components' +import illustrationSrc from '../../../assets/illustrations/le-jobs-s3.png' +import { + Application, + ListApplicationsQueryResult, +} from '../../../graphql/types/schema' +import { useBrowser } from '../../../lib/use-browser' +import { getApplicationUrl } from '../../../utils/applications-utils' +import { BottomTabsIndicator } from '../../../components/bottom-tabs-indicator/bottom-tabs-indicator' +import { createSkeletonArr } from '../../../utils/create-skeleton-arr' +import { getBadgeVariant } from '../utils/getBadgeVariant' + +type FlatListItem = + | Application + | { __typename: 'Skeleton'; id: string } + | { __typename: 'Empty'; id: string } + +interface ApplicationsListProps { + applicationsRes: ListApplicationsQueryResult + componentId: string + displayProgress: boolean + displayDescription: boolean + onRefetch: (refetching: boolean) => void +} + +export const ApplicationsList = ({ + applicationsRes, + componentId, + displayDescription, + displayProgress, + onRefetch, +}: ApplicationsListProps) => { + const { openBrowser } = useBrowser() + const theme = useTheme() + const [refetching, setRefetching] = useState(false) + const intl = useIntl() + const flatListRef = useRef(null) + const scrollY = useRef(new Animated.Value(0)).current + + const applications = useMemo( + () => applicationsRes.data?.applicationApplications ?? [], + [applicationsRes], + ) + + const showError = applicationsRes.error && !applicationsRes.data + + const onRefresh = useCallback(async () => { + setRefetching(true) + onRefetch(true) + + try { + await applicationsRes.refetch() + } catch (e) { + // noop + } finally { + setRefetching(false) + onRefetch(false) + } + }, [applicationsRes]) + + const renderItem = useCallback( + ({ item }: ListRenderItemInfo) => { + if (item.__typename === 'Skeleton') { + return ( + + + + ) + } + + if (item.__typename === 'Empty') { + return ( + + + } + /> + + ) + } + + const badgeVariant = + item?.actionCard?.tag?.variant ?? getBadgeVariant(item) + + return ( + + } + progress={ + displayProgress + ? item.actionCard?.draftFinishedSteps ?? 0 + : undefined + } + progressTotalSteps={ + displayProgress ? item.actionCard?.draftTotalSteps ?? 0 : undefined + } + progressMessage={ + displayProgress + ? intl.formatMessage( + { + id: 'applicationStatusCard.draftProgress', + }, + { + draftFinishedSteps: item.actionCard?.draftFinishedSteps, + draftTotalSteps: item.actionCard?.draftTotalSteps, + }, + ) + : undefined + } + description={ + displayDescription + ? item.actionCard?.pendingAction?.title ?? + intl.formatMessage( + { id: 'applicationStatusCard.description' }, + { state: item.status || 'unknown' }, + ) + : undefined + } + actions={[ + { + text: intl.formatMessage({ + id: 'applicationStatusCard.openButtonLabel', + }), + onPress() { + openBrowser(getApplicationUrl(item), componentId) + }, + }, + ]} + institution={item.institution ?? ''} + style={{ marginHorizontal: theme.spacing[2] }} + /> + ) + }, + [theme], + ) + + const keyExtractor = useCallback((item: FlatListItem) => { + return item.id.toString() + }, []) + + const data = useMemo(() => { + if (applicationsRes.loading && !applicationsRes.data) { + return createSkeletonArr(5) + } + if (applications.length === 0) { + return [{ id: '0', __typename: 'Empty' }] + } + return applications + }, [ + applicationsRes.loading, + applicationsRes.data, + applications, + ]) as FlatListItem[] + + return ( + + {showError ? ( + + ) : ( + <> + + } + scrollEventThrottle={16} + scrollToOverflowEnabled + onScroll={Animated.event( + [{ nativeEvent: { contentOffset: { y: scrollY } } }], + { + useNativeDriver: true, + }, + )} + data={data} + keyExtractor={keyExtractor} + renderItem={renderItem} + /> + + + + )} + + ) +} diff --git a/apps/native/app/src/screens/applications/components/applications-preview.tsx b/apps/native/app/src/screens/applications/components/applications-preview.tsx new file mode 100644 index 000000000000..cec7b6fd35c5 --- /dev/null +++ b/apps/native/app/src/screens/applications/components/applications-preview.tsx @@ -0,0 +1,168 @@ +import { + Badge, + badgeColorSchemes, + ChevronRight, + Heading, + StatusCard, + Typography, + ViewPager, +} from '@ui' +import { useIntl } from 'react-intl' +import { TouchableOpacity, View } from 'react-native' +import { useTheme } from 'styled-components' + +import { Application } from '../../../graphql/types/schema' +import { getApplicationType } from '../utils/getApplicationType' +import { getBadgeVariant } from '../utils/getBadgeVariant' +import { useBrowser } from '../../../lib/use-browser' +import { getApplicationUrl } from '../../../utils/applications-utils' +import { navigateTo } from '../../../lib/deep-linking' +import { screenWidth } from '../../../utils/dimensions' + +interface ApplicationsPreviewProps { + componentId: string + applications: Application[] + headingTitleId: string + headingTitleNavigationLink: string + numberOfItems?: number + slider?: boolean +} + +export const ApplicationsPreview = ({ + componentId, + headingTitleId, + headingTitleNavigationLink, + applications, + numberOfItems = 4, + slider = false, +}: ApplicationsPreviewProps) => { + const { openBrowser } = useBrowser() + const theme = useTheme() + const intl = useIntl() + + const count = applications.length + + const getApplications = ( + applications: Application[], + numberOfItems: number, + ) => { + return applications.slice(0, numberOfItems).map((application) => { + const type = getApplicationType(application) + const badgeVariant = + application?.actionCard?.tag?.variant ?? getBadgeVariant(application) + + return ( + + } + progress={ + type !== 'incomplete' + ? undefined + : application.actionCard?.draftFinishedSteps ?? 0 + } + progressTotalSteps={application.actionCard?.draftTotalSteps ?? 0} + progressMessage={intl.formatMessage( + { + id: 'applicationStatusCard.draftProgress', + }, + { + draftFinishedSteps: application.actionCard?.draftFinishedSteps, + draftTotalSteps: application.actionCard?.draftTotalSteps, + }, + )} + progressContainerWidth={ + slider ? screenWidth - theme.spacing[2] * 6 : undefined + } + description={ + type !== 'incomplete' + ? application.actionCard?.pendingAction?.title ?? + intl.formatMessage( + { id: 'applicationStatusCard.description' }, + { state: application.status || 'unknown' }, + ) + : undefined + } + actions={[ + { + text: intl.formatMessage({ + id: 'applicationStatusCard.openButtonLabel', + }), + onPress() { + openBrowser(getApplicationUrl(application), componentId) + }, + }, + ]} + institution={application.institution ?? ''} + style={ + slider && count > 1 + ? { + width: screenWidth - theme.spacing[2] * 4, + marginLeft: theme.spacing[2], + } + : { + marginHorizontal: theme.spacing[2], + } + } + /> + ) + }) + } + + const items = getApplications(applications, numberOfItems) + + return ( + <> + {applications.length > 0 ? ( + navigateTo(`${headingTitleNavigationLink}`)} + style={{ marginHorizontal: theme.spacing[2] }} + > + numberOfItems) ? ( + navigateTo(`${headingTitleNavigationLink}`)} + style={{ + flexDirection: 'row', + alignItems: 'center', + }} + > + + {intl.formatMessage({ id: 'button.seeAll' })} + + + + ) : null + } + > + {intl.formatMessage({ id: `${headingTitleId}` })} + + + ) : null} + {!slider || count === 1 ? ( + items + ) : ( + + {items} + + )} + + ) +} diff --git a/apps/native/app/src/screens/applications/utils/getApplicationType.ts b/apps/native/app/src/screens/applications/utils/getApplicationType.ts new file mode 100644 index 000000000000..7047011ee59c --- /dev/null +++ b/apps/native/app/src/screens/applications/utils/getApplicationType.ts @@ -0,0 +1,21 @@ +import { + Application, + ApplicationResponseDtoStatusEnum, +} from '../../../graphql/types/schema' + +type ApplicationType = 'incomplete' | 'completed' | 'inProgress' + +export const getApplicationType = ( + application: Application, +): ApplicationType => { + switch (application.status) { + case ApplicationResponseDtoStatusEnum.Draft: + return 'incomplete' + case ApplicationResponseDtoStatusEnum.Notstarted: + return 'incomplete' + case ApplicationResponseDtoStatusEnum.Inprogress: + return 'inProgress' + default: + return 'completed' + } +} diff --git a/apps/native/app/src/screens/applications/utils/getBadgeVariant.ts b/apps/native/app/src/screens/applications/utils/getBadgeVariant.ts new file mode 100644 index 000000000000..ae8397301275 --- /dev/null +++ b/apps/native/app/src/screens/applications/utils/getBadgeVariant.ts @@ -0,0 +1,21 @@ +import { + Application, + ApplicationResponseDtoStatusEnum, +} from '../../../graphql/types/schema' + +type BadgeVariant = 'blue' | 'blueberry' | 'mint' | 'red' + +export const getBadgeVariant = (application: Application): BadgeVariant => { + switch (application.status) { + case ApplicationResponseDtoStatusEnum.Draft: + return 'blue' + case ApplicationResponseDtoStatusEnum.Notstarted: + return 'blueberry' + case ApplicationResponseDtoStatusEnum.Inprogress: + return 'blueberry' + case ApplicationResponseDtoStatusEnum.Rejected: + return 'red' + default: + return 'mint' + } +} diff --git a/apps/native/app/src/screens/home/applications-module.tsx b/apps/native/app/src/screens/home/applications-module.tsx index 2c30442b7f25..076b53ec768e 100644 --- a/apps/native/app/src/screens/home/applications-module.tsx +++ b/apps/native/app/src/screens/home/applications-module.tsx @@ -1,38 +1,23 @@ -import { - Badge, - ChevronRight, - EmptyCard, - Heading, - LinkText, - StatusCard, - StatusCardSkeleton, - Typography, - ViewPager, - blue400, -} from '@ui' +import { EmptyCard, StatusCardSkeleton } from '@ui' import React from 'react' import { useIntl } from 'react-intl' -import { Image, SafeAreaView, TouchableOpacity } from 'react-native' -import { useTheme } from 'styled-components' +import { Image, SafeAreaView } from 'react-native' import { ApolloError } from '@apollo/client' import leJobss3 from '../../assets/illustrations/le-jobs-s3.png' import { + Application, ListApplicationsQuery, useListApplicationsQuery, } from '../../graphql/types/schema' -import { navigateTo } from '../../lib/deep-linking' -import { useBrowser } from '../../lib/use-browser' -import { getApplicationUrl } from '../../utils/applications-utils' -import { screenWidth } from '../../utils/dimensions' + +import { ApplicationsPreview } from '../applications/components/applications-preview' interface ApplicationsModuleProps { data: ListApplicationsQuery | undefined loading: boolean error?: ApolloError | undefined componentId: string - hideAction?: boolean - hideSeeAllButton?: boolean } const validateApplicationsInitialData = ({ @@ -53,92 +38,17 @@ const validateApplicationsInitialData = ({ } const ApplicationsModule = React.memo( - ({ - data, - loading, - error, - componentId, - hideAction, - hideSeeAllButton = false, - }: ApplicationsModuleProps) => { + ({ data, loading, error, componentId }: ApplicationsModuleProps) => { const intl = useIntl() - const theme = useTheme() const applications = data?.applicationApplications ?? [] const count = applications.length - const { openBrowser } = useBrowser() if (error && !data) { return null } - const viewPagerItemWidth = screenWidth - theme.spacing[2] * 4 - - const items = applications.slice(0, 3).map((application) => ( - - } - progress={(application.progress ?? 0) * 100} - actions={[ - { - text: intl.formatMessage({ - id: 'applicationStatusCard.openButtonLabel', - }), - onPress() { - openBrowser(getApplicationUrl(application), componentId) - }, - }, - ]} - style={ - count > 1 - ? { - width: viewPagerItemWidth, - marginLeft: 16, - } - : {} - } - /> - )) - return ( - - navigateTo(`/applications`)} - > - navigateTo('/applications')} - style={{ - flexDirection: 'row', - alignItems: 'center', - }} - > - - {intl.formatMessage({ id: 'button.seeAll' })} - - - - ) - } - > - {intl.formatMessage({ id: 'home.applicationsStatus' })} - - + {loading && !data ? ( ) : ( @@ -146,7 +56,7 @@ const ApplicationsModule = React.memo( {count === 0 && ( } - link={ - hideAction ? null : ( - navigateTo(`/applications`)} - > - - {intl.formatMessage({ - id: 'applicationStatusCard.seeMoreApplications', - })} - - - ) - } + link={null} /> )} - {count === 1 && items} - {count >= 2 && ( - {items} + {count !== 0 && ( + )} )} diff --git a/apps/native/app/src/ui/index.ts b/apps/native/app/src/ui/index.ts index 40700141149e..22ebf65c8fa2 100644 --- a/apps/native/app/src/ui/index.ts +++ b/apps/native/app/src/ui/index.ts @@ -58,4 +58,5 @@ export * from './lib/link/link-text' export * from './lib/typography/typography' export * from './lib/scan-result-card/scan-result-card' export * from './lib/label/label' +export * from './lib/progress-meter/progress-meter' export * from './utils/index' diff --git a/apps/native/app/src/ui/lib/badge/badge.tsx b/apps/native/app/src/ui/lib/badge/badge.tsx index 50c0a211722f..d6d91289ec07 100644 --- a/apps/native/app/src/ui/lib/badge/badge.tsx +++ b/apps/native/app/src/ui/lib/badge/badge.tsx @@ -1,37 +1,81 @@ import React from 'react' -import styled from 'styled-components/native' -import { dynamicColor } from '../../utils' -import { font } from '../../utils/font' +import styled, { useTheme } from 'styled-components/native' +import { Typography } from '../typography/typography' const Host = styled.View` overflow: hidden; border-radius: ${({ theme }) => theme.border.radius.standard}; - background-color: ${dynamicColor(({ theme }) => ({ - light: theme.color.roseTinted100, - dark: theme.shades.dark.shade300, - }))}; - padding: 5px 7px; + padding: ${({ theme }) => theme.spacing[1]}px; ` -const Text = styled.Text` - ${font({ - fontSize: 13, - fontWeight: '600', - color: ({ theme }) => ({ - light: theme.color.roseTinted400, - dark: theme.color.roseTinted200, - }), - })} -` +export const badgeColorSchemes = { + blue: { + color: 'blue400', + backgroundColor: 'blue100', + }, + mint: { + color: 'mint800', + backgroundColor: 'mint200', + }, + blueberry: { + color: 'blueberry400', + backgroundColor: 'blueberry100', + }, + darkerBlue: { + color: 'blue600', + backgroundColor: 'blue200', + }, + white: { + color: 'blue400', + backgroundColor: 'white', + }, + purple: { + color: 'purple400', + backgroundColor: 'purple100', + }, + red: { + color: 'red600', + backgroundColor: 'red100', + }, + rose: { + color: 'roseTinted400', + backgroundColor: 'roseTinted100', + }, + dark: { + color: 'dark400', + backgroundColor: 'dark200', + }, + yellow: { + color: 'dark400', + backgroundColor: 'yellow400', + }, + disabled: { + color: 'dark200', + backgroundColor: 'dark100', + }, + warn: { + color: 'dark400', + backgroundColor: 'yellow200', + }, +} as const interface BadgeProps { title: string + variant: keyof typeof badgeColorSchemes } -export function Badge({ title }: BadgeProps) { +export function Badge({ title, variant = 'blue' }: BadgeProps) { + const theme = useTheme() + const badgeVariant = badgeColorSchemes[variant] ?? 'blue' return ( - - {title} + + + {title} + ) } diff --git a/apps/native/app/src/ui/lib/card/status-card.tsx b/apps/native/app/src/ui/lib/card/status-card.tsx index 8a4b2fcc2485..2e7f89d32f60 100644 --- a/apps/native/app/src/ui/lib/card/status-card.tsx +++ b/apps/native/app/src/ui/lib/card/status-card.tsx @@ -1,14 +1,15 @@ import React from 'react' import { FormattedDate } from 'react-intl' import { Image, ImageSourcePropType, View, ViewStyle } from 'react-native' -import styled from 'styled-components/native' +import styled, { useTheme } from 'styled-components/native' import timeOutlineIcon from '../../assets/card/time-outline.png' import { dynamicColor } from '../../utils/dynamic-color' -import { font } from '../../utils/font' +import { Typography } from '../typography/typography' +import { useOrganizationsStore } from '../../../stores/organizations-store' +import { ProgressMeter } from '../progress-meter/progress-meter' const Host = styled.View` - width: 100%; - min-height: 160px; + min-height: 180px; border-radius: ${({ theme }) => theme.border.radius.large}; border-width: ${({ theme }) => theme.border.width.standard}px; border-color: ${dynamicColor( @@ -22,6 +23,8 @@ const Host = styled.View` ` const ActionsContainer = styled.View` + justify-content: center; + align-items: center; border-top-width: ${({ theme }) => theme.border.width.standard}px; border-top-color: ${dynamicColor( (props) => ({ @@ -34,9 +37,6 @@ const ActionsContainer = styled.View` ` const ActionButton = styled.TouchableOpacity<{ border: boolean }>` - flex: 1; - align-items: center; - justify-content: center; padding: ${({ theme }) => theme.spacing[2]}px; border-left-width: ${({ theme }) => theme.border.width.standard}px; border-left-color: ${dynamicColor( @@ -45,35 +45,16 @@ const ActionButton = styled.TouchableOpacity<{ border: boolean }>` )}; ` -const ActionText = styled.Text` - ${font({ - fontWeight: '600', - color: ({ theme }) => theme.color.blue400, - })} - text-align: center; -` - -const Title = styled.Text` - margin-bottom: ${({ theme }) => theme.spacing[1]}px; - - ${font({ - fontWeight: '600', - fontSize: 13, - lineHeight: 17, - })} -` - -const Description = styled.Text` - ${font({ - fontWeight: '300', - lineHeight: 24, - })} +const Title = styled.View` + flex-direction: row; + padding-bottom: ${({ theme }) => theme.spacing[1]}px; + align-items: center; ` const Content = styled.View` - padding: ${({ theme }) => theme.spacing[2]}px; - padding-top: 0px; flex: 1; + padding: ${({ theme }) => theme.spacing[2]}px; + padding-top: 0; ` const Date = styled.View` @@ -81,47 +62,22 @@ const Date = styled.View` align-items: center; ` -const DateText = styled.Text` - ${font({ - fontWeight: '300', - fontSize: 13, - lineHeight: 17, - })} -` - const Row = styled.View` flex-direction: row; justify-content: space-between; padding: ${({ theme }) => theme.spacing[2]}px; ` -const Bar = styled.View` - height: 12px; - padding: 2px; - overflow: hidden; - background-color: ${dynamicColor(({ theme }) => ({ - dark: theme.color.roseTinted600, - light: theme.color.roseTinted200, - }))}; - border-radius: 6px; - - margin-top: ${({ theme }) => theme.spacing[2]}px; -` - -const Progress = styled.View<{ width?: number }>` - flex: 1; - width: ${(props) => props.width ?? 0}%; - border-radius: 6px; - - background-color: ${dynamicColor(({ theme }) => theme.color.roseTinted400)}; -` - interface StatusCardProps { title: string description?: string date: Date badge?: React.ReactNode progress?: number + progressTotalSteps?: number + progressMessage?: string + progressContainerWidth?: number + institution?: string actions: Array<{ text: string; onPress(): void }> style?: ViewStyle } @@ -134,34 +90,61 @@ export function StatusCard({ progress, actions = [], style, + institution, + progressTotalSteps, + progressMessage, + progressContainerWidth, }: StatusCardProps) { + const { getOrganizationLogoUrl } = useOrganizationsStore() + const icon = getOrganizationLogoUrl(institution ?? '') + const theme = useTheme() + return ( - + - + {badge} - {title} - {!!description && {description}} - - - - + + <Image + source={icon} + style={{ width: 24, height: 24, marginRight: theme.spacing[1] }} + /> + <Typography variant="heading5" style={{ flexShrink: 1 }}> + {title} + </Typography> + + {!!description && {description}} + {!!progress && ( + + )} + {actions.length ? ( {actions.map(({ text, onPress }, i) => ( - {text} + + {text} + ))} diff --git a/apps/native/app/src/ui/lib/problem/problem-template.tsx b/apps/native/app/src/ui/lib/problem/problem-template.tsx index 2503912432dd..b15358fbdc05 100644 --- a/apps/native/app/src/ui/lib/problem/problem-template.tsx +++ b/apps/native/app/src/ui/lib/problem/problem-template.tsx @@ -83,15 +83,17 @@ const Host = styled.View<{ min-height: 280px; ` -const Tag = styled(Typography)<{ +const Tag = styled(View)<{ backgroundColor: Colors - color?: Colors }>` background-color: ${({ backgroundColor, theme }) => theme.color[backgroundColor]}; padding: ${({ theme }) => theme.spacing[1]}px; border-radius: ${({ theme }) => theme.border.radius.large}; overflow: hidden; +` + +const TagText = styled(Typography)<{ color?: Colors }>` ${({ color, theme }) => color && `color: ${theme.color[color]};`} ` @@ -119,12 +121,10 @@ export const ProblemTemplate = ({ return ( {tag && ( - - {tag} + + + {tag} + )} {showIcon && } diff --git a/apps/native/app/src/ui/lib/progress-meter/progress-meter.tsx b/apps/native/app/src/ui/lib/progress-meter/progress-meter.tsx new file mode 100644 index 000000000000..47d47d148282 --- /dev/null +++ b/apps/native/app/src/ui/lib/progress-meter/progress-meter.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import { View } from 'react-native' +import { screenWidth } from '../../../utils/dimensions' +import styled, { useTheme } from 'styled-components/native' +import { Typography } from '../typography/typography' + +const Host = styled.View` + border-radius: ${({ theme: { border } }) => border.radius.extraLarge}; + height: 16px; + padding: ${({ theme }) => theme.spacing.smallGutter}px; + margin-top: ${({ theme }) => theme.spacing[1]}px; +` + +const InnerHost = styled.View` + flex: 1; + flex-direction: row; + column-gap: ${({ theme }) => theme.spacing.smallGutter}px; + border-radius: ${({ theme: { border } }) => border.radius.large}; + height: 100%; +` + +export type ProgressMeterVariant = 'blue' | 'red' | 'rose' | 'mint' + +interface ProgressMeterProps { + totalSteps: number + finishedSteps: number + containerWidth?: number + variant?: ProgressMeterVariant + progressMessage?: string +} + +const colorSchemes = { + blue: { + unfinished: 'blue200', + outer: 'blue100', + inner: 'blue400', + }, + red: { + unfinished: 'red200', + outer: 'red100', + inner: 'red400', + }, + rose: { + unfinished: 'roseTinted200', + outer: 'roseTinted100', + inner: 'roseTinted400', + }, + mint: { + unfinished: 'mint200', + outer: 'mint100', + inner: 'mint600', + }, +} as const + +export const ProgressMeter = ({ + finishedSteps, + totalSteps, + progressMessage, + containerWidth, + variant = 'blue', +}: ProgressMeterProps) => { + const theme = useTheme() + const steps = Array.from(Array(totalSteps ?? 1).keys()) + const allowedWidth = containerWidth ?? screenWidth - theme.spacing[2] * 4 + + if (!totalSteps) { + return + } + // Take into account padding in each end and between each step + const stepWidth = + (allowedWidth - + theme.spacing.smallGutter * 2 - + totalSteps * theme.spacing.smallGutter) / + totalSteps + + return ( + <> + + + {steps.map((i) => ( + i + ? theme.color[colorSchemes[variant].inner] + : theme.color[colorSchemes[variant].unfinished], + borderRadius: 8, + width: stepWidth, + }} + key={`draft-progress-meter-${i}`} + /> + ))} + + + + + {progressMessage} + + + + ) +} diff --git a/apps/native/app/src/utils/component-registry.ts b/apps/native/app/src/utils/component-registry.ts index 7e80ba888c8d..2b695b608411 100644 --- a/apps/native/app/src/utils/component-registry.ts +++ b/apps/native/app/src/utils/component-registry.ts @@ -18,6 +18,9 @@ export const ComponentRegistry = { HomeOptionsScreen: `${prefix}.screens.HomeOptions`, InboxScreen: `${prefix}.screens.Inbox`, ApplicationsScreen: `${prefix}.screens.ApplicationsScreen`, + ApplicationsCompletedScreen: `${prefix}.screens.ApplicationsCompletedScreen`, + ApplicationsInProgressScreen: `${prefix}.screens.ApplicationsInProgressScreen`, + ApplicationsIncompleteScreen: `${prefix}.screens.ApplicationsIncompleteScreen`, WalletScreen: `${prefix}.screens.Wallet`, WalletPassScreen: `${prefix}.screens.WalletPass`, WalletPassportScreen: `${prefix}.screens.WalletPassport`, diff --git a/apps/native/app/src/utils/lifecycle/setup-components.tsx b/apps/native/app/src/utils/lifecycle/setup-components.tsx index 8a90b927b4df..06e456cab432 100644 --- a/apps/native/app/src/utils/lifecycle/setup-components.tsx +++ b/apps/native/app/src/utils/lifecycle/setup-components.tsx @@ -4,6 +4,9 @@ import { isTestingApp } from '../../config' import { AirDiscountScreen } from '../../screens/air-discount/air-discount' import { AppLockScreen } from '../../screens/app-lock/app-lock' import { ApplicationsScreen } from '../../screens/applications/applications' +import { ApplicationsCompletedScreen } from '../../screens/applications/applications-completed' +import { ApplicationsInProgressScreen } from '../../screens/applications/applications-in-progress' +import { ApplicationsIncompleteScreen } from '../../screens/applications/applications-incomplete' import { AssetsDetailScreen } from '../../screens/assets/assets-detail' import { AssetsOverviewScreen } from '../../screens/assets/assets-overview' import { CognitoAuthScreen } from '../../screens/cognito-auth/cognito-auth' @@ -101,6 +104,15 @@ export function registerAllComponents() { registerComponent(CR.AirDiscountScreen, AirDiscountScreen) registerComponent(CR.PasskeyScreen, PasskeyScreen) registerComponent(CR.HomeOptionsScreen, HomeOptionsScreen) + registerComponent(CR.ApplicationsCompletedScreen, ApplicationsCompletedScreen) + registerComponent( + CR.ApplicationsInProgressScreen, + ApplicationsInProgressScreen, + ) + registerComponent( + CR.ApplicationsIncompleteScreen, + ApplicationsIncompleteScreen, + ) // Overlay registerComponent(CR.OfflineBanner, OfflineBanner) diff --git a/apps/native/app/src/utils/lifecycle/setup-routes.ts b/apps/native/app/src/utils/lifecycle/setup-routes.ts index 54a7d80a3344..a3efb5a76748 100644 --- a/apps/native/app/src/utils/lifecycle/setup-routes.ts +++ b/apps/native/app/src/utils/lifecycle/setup-routes.ts @@ -51,11 +51,47 @@ export function setupRoutes() { selectTab(4) }) - addRoute('/applications', () => { + addRoute('/applications', async () => { Navigation.dismissAllModals() selectTab(3) }) + addRoute('/applications-completed', async (passProps) => { + Navigation.dismissAllModals() + selectTab(3) + await Navigation.popToRoot(StackRegistry.ApplicationsStack) + await Navigation.push(ComponentRegistry.ApplicationsScreen, { + component: { + name: ComponentRegistry.ApplicationsCompletedScreen, + passProps, + }, + }) + }) + + addRoute('/applications-in-progress', async (passProps) => { + Navigation.dismissAllModals() + selectTab(3) + await Navigation.popToRoot(StackRegistry.ApplicationsStack) + await Navigation.push(ComponentRegistry.ApplicationsScreen, { + component: { + name: ComponentRegistry.ApplicationsInProgressScreen, + passProps, + }, + }) + }) + + addRoute('/applications-incomplete', async (passProps) => { + Navigation.dismissAllModals() + selectTab(3) + await Navigation.popToRoot(StackRegistry.ApplicationsStack) + await Navigation.push(ComponentRegistry.ApplicationsScreen, { + component: { + name: ComponentRegistry.ApplicationsIncompleteScreen, + passProps, + }, + }) + }) + addRoute('/vehicles', async (passProps) => { selectTab(4) await Navigation.dismissAllModals() From 3e2136e78a8bd4705d90caa959e374786382d1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 20 Sep 2024 08:56:42 +0000 Subject: [PATCH 095/173] fix(web): Pension Calculator - Limit how far into future start month can be (#16094) * Limit how far into future months can be * Add more robust method * Reuse variable --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../PensionCalculator.tsx | 124 ++++++++++++------ 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx b/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx index efc9ed065e6c..f72249630a5a 100644 --- a/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx +++ b/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx @@ -159,6 +159,11 @@ const PensionCalculator: CustomScreen = ({ ? 12 * 7 : 12 * 2 + const maxMonthPensionDelay = + typeof birthYear === 'number' && birthYear < 1952 + ? maxMonthPensionDelayIfBorn1951OrEarlier + : maxMonthPensionDelayIfBornAfter1951 + const basePensionTypeOptions = useMemo[]>(() => { const options = [ { @@ -390,11 +395,6 @@ const PensionCalculator: CustomScreen = ({ months: -maxMonthPensionHurry, }).getFullYear() - const maxMonthPensionDelay = - typeof birthYear === 'number' && birthYear < 1952 - ? maxMonthPensionDelayIfBorn1951OrEarlier - : maxMonthPensionDelayIfBornAfter1951 - const maxYear = add(defaultPensionDate, { months: maxMonthPensionDelay, }).getFullYear() @@ -408,13 +408,7 @@ const PensionCalculator: CustomScreen = ({ } return options - }, [ - birthYear, - defaultPensionDate, - maxMonthPensionDelayIfBorn1951OrEarlier, - maxMonthPensionDelayIfBornAfter1951, - maxMonthPensionHurry, - ]) + }, [defaultPensionDate, maxMonthPensionDelay, maxMonthPensionHurry]) const title = `${formatMessage(translationStrings.mainTitle)} ${ dateOfCalculationsOptions.find((o) => o.value === dateOfCalculations) @@ -422,22 +416,31 @@ const PensionCalculator: CustomScreen = ({ }` const startMonthOptions = useMemo(() => { - if ( - startYear === startYearOptions?.[0]?.value && - typeof birthMonth === 'number' && - typeof startMonth === 'number' - ) { - if (startMonth < birthMonth + 1) { - methods.setValue('startMonth', birthMonth + 1) - } - return monthOptions.filter(({ value }) => value >= birthMonth + 1) + if (!defaultPensionDate) { + return monthOptions } + + if (startYear === startYearOptions[0]?.value) { + const minMonth = add(defaultPensionDate, { + months: -maxMonthPensionHurry, + }).getMonth() + return monthOptions.filter((month) => month.value >= minMonth) + } + + if (startYear === startYearOptions[startYearOptions.length - 1]?.value) { + const maxMonth = add(defaultPensionDate, { + months: maxMonthPensionDelay, + }).getMonth() + + return monthOptions.filter((month) => month.value <= maxMonth) + } + return monthOptions }, [ - birthMonth, - methods, + defaultPensionDate, + maxMonthPensionDelay, + maxMonthPensionHurry, monthOptions, - startMonth, startYear, startYearOptions, ]) @@ -600,20 +603,16 @@ const PensionCalculator: CustomScreen = ({ translationStrings.birthMonthPlaceholder, )} onSelect={(option) => { - if (option.value > 10) { - methods.setValue('startMonth', 0) - if (startYear) { - methods.setValue( - 'startYear', - startYear + 1, - ) - } - } else { - methods.setValue( - 'startMonth', - option.value + 1, - ) - } + methods.setValue( + 'startMonth', + option.value > 10 ? 0 : option.value + 1, + ) + methods.setValue( + 'startYear', + birthYear + + defaultPensionAge + + (option.value > 10 ? 1 : 0), + ) }} /> @@ -694,6 +693,55 @@ const PensionCalculator: CustomScreen = ({ placeholder={formatMessage( translationStrings.startYearPlaceholder, )} + onSelect={(option) => { + if (!defaultPensionDate) { + return + } + if ( + option.value === + startYearOptions[0]?.value + ) { + const minMonth = add( + defaultPensionDate, + { + months: -maxMonthPensionHurry, + }, + ).getMonth() + if ( + typeof startMonth === 'number' && + startMonth < minMonth + ) { + methods.setValue( + 'startMonth', + minMonth, + ) + } + } + + if ( + option.value === + startYearOptions[ + startYearOptions.length - 1 + ]?.value + ) { + const maxMonth = add( + defaultPensionDate, + { + months: maxMonthPensionDelay, + }, + ).getMonth() + + if ( + typeof startMonth === 'number' && + startMonth > maxMonth + ) { + methods.setValue( + 'startMonth', + maxMonth, + ) + } + } + }} /> From 549f2888ad1fad031f1a496ce28c07fe56de827b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3n=20Bjarni=20=C3=93lafsson?= <92530555+jonbjarnio@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:15:03 +0000 Subject: [PATCH 096/173] feat(ojoi): Add auth to client endpoint (#16090) * Added auth to application endpoints * Removed console logs and updated logging level to appropriate one --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/ojoiApplication.resolver.ts | 50 ++++--- .../src/lib/ojoiApplication.service.ts | 125 +++++++++++------- .../official-journal-of-iceland.service.ts | 9 +- .../src/lib/ojoiApplicationClient.config.ts | 4 +- .../src/lib/ojoiApplicationClient.provider.ts | 13 +- .../src/lib/ojoiApplicationClient.service.ts | 81 +++++++++--- 6 files changed, 187 insertions(+), 95 deletions(-) diff --git a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts index 8d7c5cee47f7..9ea5f38765d8 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts @@ -1,5 +1,10 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { IdsUserGuard, Scopes, ScopesGuard } from '@island.is/auth-nest-tools' +import { + CurrentUser, + IdsUserGuard, + Scopes, + ScopesGuard, +} from '@island.is/auth-nest-tools' import { ApiScope } from '@island.is/auth/scopes' import { FeatureFlag, Features } from '@island.is/nest/feature-flags' import { OfficialJournalOfIcelandApplicationService } from './ojoiApplication.service' @@ -7,7 +12,6 @@ import { GetCommentsInput } from '../models/getComments.input' import { GetCommentsResponse } from '../models/getComments.response' import { PostCommentInput } from '../models/postComment.input' import { PostCommentResponse } from '../models/postComment.response' -import { PostApplicationInput } from '../models/postApplication.input' import { UseGuards } from '@nestjs/common' import { CaseGetPriceResponse } from '../models/getPrice.response' import { GetPdfUrlResponse } from '../models/getPdfUrlResponse' @@ -18,6 +22,7 @@ import { AddApplicationAttachmentInput } from '../models/addApplicationAttachmen import { GetApplicationAttachmentInput } from '../models/getApplicationAttachment.input' import { GetApplicationAttachmentsResponse } from '../models/getApplicationAttachments.response' import { DeleteApplicationAttachmentInput } from '../models/deleteApplicationAttachment.input' +import type { User } from '@island.is/auth-nest-tools' @Scopes(ApiScope.internal) @UseGuards(IdsUserGuard, ScopesGuard) @@ -31,36 +36,35 @@ export class OfficialJournalOfIcelandApplicationResolver { @Query(() => GetCommentsResponse, { name: 'officialJournalOfIcelandApplicationGetComments', }) - getComments(@Args('input') input: GetCommentsInput) { - return this.ojoiApplicationService.getComments(input) + getComments( + @Args('input') input: GetCommentsInput, + @CurrentUser() user: User, + ) { + return this.ojoiApplicationService.getComments(input, user) } @Mutation(() => PostCommentResponse, { name: 'officialJournalOfIcelandApplicationPostComment', }) - postComment(@Args('input') input: PostCommentInput) { - return this.ojoiApplicationService.postComment(input) - } - - @Query(() => Boolean, { - name: 'officialJournalOfIcelandApplicationPostApplication', - }) - postApplication(@Args('input') input: PostApplicationInput) { - return this.ojoiApplicationService.postApplication(input) + postComment( + @Args('input') input: PostCommentInput, + @CurrentUser() user: User, + ) { + return this.ojoiApplicationService.postComment(input, user) } @Query(() => CaseGetPriceResponse, { name: 'officialJournalOfIcelandApplicationGetPrice', }) - getPrice(@Args('id') id: string) { - return this.ojoiApplicationService.getPrice(id) + getPrice(@Args('id') id: string, @CurrentUser() user: User) { + return this.ojoiApplicationService.getPrice(id, user) } @Query(() => GetPdfUrlResponse, { name: 'officialJournalOfIcelandApplicationGetPdfUrl', }) - getPdfUrl(@Args('id') id: string) { - return this.ojoiApplicationService.getPdfUrl(id) + getPdfUrl(@Args('id') id: string, @CurrentUser() user: User) { + return this.ojoiApplicationService.getPdfUrl(id, user) } @Mutation(() => GetPresignedUrlResponse, { @@ -69,8 +73,9 @@ export class OfficialJournalOfIcelandApplicationResolver { getPresignedUrl( @Args('input', { type: () => GetPresignedUrlInput }) input: GetPresignedUrlInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.getPresignedUrl(input) + return this.ojoiApplicationService.getPresignedUrl(input, user) } @Mutation(() => AddApplicationAttachmentResponse, { @@ -79,8 +84,9 @@ export class OfficialJournalOfIcelandApplicationResolver { addAttachment( @Args('input', { type: () => AddApplicationAttachmentInput }) input: AddApplicationAttachmentInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.addApplicationAttachment(input) + return this.ojoiApplicationService.addApplicationAttachment(input, user) } @Query(() => GetApplicationAttachmentsResponse, { @@ -89,8 +95,9 @@ export class OfficialJournalOfIcelandApplicationResolver { getAttachments( @Args('input', { type: () => GetApplicationAttachmentInput }) input: AddApplicationAttachmentInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.getApplicationAttachments(input) + return this.ojoiApplicationService.getApplicationAttachments(input, user) } @Mutation(() => AddApplicationAttachmentResponse, { @@ -99,7 +106,8 @@ export class OfficialJournalOfIcelandApplicationResolver { deleteAttachment( @Args('input', { type: () => DeleteApplicationAttachmentInput }) input: DeleteApplicationAttachmentInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.deleteApplicationAttachment(input) + return this.ojoiApplicationService.deleteApplicationAttachment(input, user) } } diff --git a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts index e8b2adc91172..89b074ee1eae 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts @@ -16,6 +16,7 @@ import { GetApplicationAttachmentInput } from '../models/getApplicationAttachmen import { DeleteApplicationAttachmentInput } from '../models/deleteApplicationAttachment.input' import { LOGGER_PROVIDER } from '@island.is/logging' import type { Logger } from '@island.is/logging' +import { User } from '@island.is/auth-nest-tools' const LOG_CATEGORY = 'official-journal-of-iceland-application' @@ -27,72 +28,92 @@ export class OfficialJournalOfIcelandApplicationService { private readonly ojoiApplicationService: OfficialJournalOfIcelandApplicationClientService, ) {} - async getComments(input: GetCommentsInput) { - return this.ojoiApplicationService.getComments(input) + async getComments(input: GetCommentsInput, user: User) { + return this.ojoiApplicationService.getComments(input, user) } - async postComment(input: PostCommentInput) { - const success = this.ojoiApplicationService.postComment({ - id: input.id, - postApplicationComment: { - comment: input.comment, + async postComment(input: PostCommentInput, user: User) { + const success = this.ojoiApplicationService.postComment( + { + id: input.id, + postApplicationComment: { + comment: input.comment, + }, }, - }) + user, + ) return { success, } } - async getPdfUrl(id: string) { - return this.ojoiApplicationService.getPdfUrl({ - id, - }) + async getPdfUrl(id: string, user: User) { + return this.ojoiApplicationService.getPdfUrl( + { + id, + }, + user, + ) } - async postApplication(input: PostApplicationInput): Promise { - return this.ojoiApplicationService.postApplication(input) + async postApplication( + input: PostApplicationInput, + user: User, + ): Promise { + return this.ojoiApplicationService.postApplication(input, user) } - async getPrice(id: string) { - return this.ojoiApplicationService.getPrice({ - id, - }) + async getPrice(id: string, user: User) { + return this.ojoiApplicationService.getPrice( + { + id, + }, + user, + ) } async getPresignedUrl( input: GetPresignedUrlInput, + user: User, ): Promise { const attachmentType = mapPresignedUrlType(input.attachmentType) - return this.ojoiApplicationService.getPresignedUrl({ - id: input.applicationId, - type: attachmentType, - getPresignedUrlBody: { - fileName: input.fileName, - fileType: input.fileType, + return this.ojoiApplicationService.getPresignedUrl( + { + id: input.applicationId, + type: attachmentType, + getPresignedUrlBody: { + fileName: input.fileName, + fileType: input.fileType, + }, }, - }) + user, + ) } async addApplicationAttachment( input: AddApplicationAttachmentInput, + user: User, ): Promise { try { const attachmentType = mapAttachmentType(input.attachmentType) - this.ojoiApplicationService.addApplicationAttachment({ - id: input.applicationId, - type: attachmentType, - postApplicationAttachmentBody: { - fileName: input.fileName, - originalFileName: input.originalFileName, - fileFormat: input.fileFormat, - fileExtension: input.fileExtension, - fileLocation: input.fileLocation, - fileSize: input.fileSize, + this.ojoiApplicationService.addApplicationAttachment( + { + id: input.applicationId, + type: attachmentType, + postApplicationAttachmentBody: { + fileName: input.fileName, + originalFileName: input.originalFileName, + fileFormat: input.fileFormat, + fileExtension: input.fileExtension, + fileLocation: input.fileLocation, + fileSize: input.fileSize, + }, }, - }) + user, + ) return { success: true, @@ -109,19 +130,31 @@ export class OfficialJournalOfIcelandApplicationService { } } - async getApplicationAttachments(input: GetApplicationAttachmentInput) { - return this.ojoiApplicationService.getApplicationAttachments({ - id: input.applicationId, - type: mapGetAttachmentType(input.attachmentType), - }) + async getApplicationAttachments( + input: GetApplicationAttachmentInput, + user: User, + ) { + return this.ojoiApplicationService.getApplicationAttachments( + { + id: input.applicationId, + type: mapGetAttachmentType(input.attachmentType), + }, + user, + ) } - async deleteApplicationAttachment(input: DeleteApplicationAttachmentInput) { + async deleteApplicationAttachment( + input: DeleteApplicationAttachmentInput, + user: User, + ) { try { - await this.ojoiApplicationService.deleteApplicationAttachment({ - id: input.applicationId, - key: input.key, - }) + await this.ojoiApplicationService.deleteApplicationAttachment( + { + id: input.applicationId, + key: input.key, + }, + user, + ) return { success: true } } catch (error) { diff --git a/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts index 0d8c5a82aadd..c652f9330b3a 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts @@ -34,9 +34,12 @@ export class OfficialJournalOfIcelandTemaplateService extends BaseTemplateApiSer async postApplication({ application, auth }: Props): Promise { try { - return await this.ojoiApplicationService.postApplication({ - id: application.id, - }) + return await this.ojoiApplicationService.postApplication( + { + id: application.id, + }, + auth, + ) } catch (error) { return false } diff --git a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts index a316d887e3dd..edaf99d628cc 100644 --- a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts +++ b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts @@ -5,8 +5,8 @@ const schema = z.object({ xRoadServicePath: z.string(), fetch: z.object({ timeout: z.number().int(), - scope: z.array(z.string()), }), + scope: z.array(z.string()), }) export const OfficialJournalOfIcelandApplicationClientConfig = defineConfig< @@ -19,9 +19,9 @@ export const OfficialJournalOfIcelandApplicationClientConfig = defineConfig< 'XROAD_OFFICIAL_JOURNAL_APPLICATION_PATH', 'IS-DEV/GOV/10014/DMR-Protected/official-journal-application', ), + scope: ['api_resource.scope'], fetch: { timeout: 10000, - scope: [], }, }), }) diff --git a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts index fdee03f0ac3e..23af8f9670db 100644 --- a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts +++ b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts @@ -6,7 +6,7 @@ import { import { createEnhancedFetch } from '@island.is/clients/middlewares' import { OfficialJournalOfIcelandApplicationClientConfig } from './ojoiApplicationClient.config' import { ConfigType } from '@nestjs/config' -import { XRoadConfig } from '@island.is/nest/config' +import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' export const OfficialJournalOfIcelandApplicationClientApiProvider: Provider = { @@ -16,11 +16,21 @@ export const OfficialJournalOfIcelandApplicationClientApiProvider: Provider, + idsClientConfig: ConfigType, ) => { return new OfficialJournalOfIcelandApplicationApi( new Configuration({ fetchApi: createEnhancedFetch({ name: 'clients-official-journal-of-iceland-application', + autoAuth: idsClientConfig.isConfigured + ? { + mode: 'tokenExchange', + issuer: idsClientConfig.issuer, + clientId: idsClientConfig.clientId, + clientSecret: idsClientConfig.clientSecret, + scope: config.scope, + } + : undefined, organizationSlug: 'domsmalaraduneytid', }), basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, @@ -34,5 +44,6 @@ export const OfficialJournalOfIcelandApplicationClientApiProvider: Provider { try { - return await this.ojoiApplicationApi.getComments(params) + return await this.ojoiApplicationApiWithAuth(auth).getComments(params) } catch (error) { - console.log(error) - this.logger.error('Failed to get comments', { + this.logger.warn('Failed to get comments', { error, applicationId: params.id, category: LOG_CATEGORY, @@ -46,12 +51,12 @@ export class OfficialJournalOfIcelandApplicationClientService { } } - async postComment(params: PostCommentRequest): Promise { + async postComment(params: PostCommentRequest, auth: Auth): Promise { try { - await this.ojoiApplicationApi.postComment(params) + await this.ojoiApplicationApiWithAuth(auth).postComment(params) return true } catch (error) { - this.logger.error(`Failed to post comment: ${error.message}`, { + this.logger.warn(`Failed to post comment: ${error.message}`, { error, applicationId: params.id, category: LOG_CATEGORY, @@ -60,12 +65,15 @@ export class OfficialJournalOfIcelandApplicationClientService { } } - async postApplication(params: PostApplicationRequest): Promise { + async postApplication( + params: PostApplicationRequest, + auth: Auth, + ): Promise { try { - await this.ojoiApplicationApi.postApplication(params) + await this.ojoiApplicationApiWithAuth(auth).postApplication(params) return Promise.resolve(true) } catch (error) { - this.logger.error('Failed to post application', { + this.logger.warn('Failed to post application', { error, applicationId: params.id, category: LOG_CATEGORY, @@ -76,14 +84,20 @@ export class OfficialJournalOfIcelandApplicationClientService { async getPdfUrl( params: GetPdfUrlByApplicationIdRequest, + auth: Auth, ): Promise { - return await this.ojoiApplicationApi.getPdfUrlByApplicationId(params) - } - - async getPdf(params: GetPdfByApplicationIdRequest): Promise { - const streamableFile = await this.ojoiApplicationApi.getPdfByApplicationId( + return await this.ojoiApplicationApiWithAuth(auth).getPdfUrlByApplicationId( params, ) + } + + async getPdf( + params: GetPdfByApplicationIdRequest, + auth: Auth, + ): Promise { + const streamableFile = await this.ojoiApplicationApiWithAuth( + auth, + ).getPdfByApplicationId(params) const isStreamable = ( streamableFile: any, @@ -106,11 +120,14 @@ export class OfficialJournalOfIcelandApplicationClientService { return Buffer.concat(chunks) } - async getPrice(params: GetPriceRequest): Promise { + async getPrice( + params: GetPriceRequest, + auth: Auth, + ): Promise { try { - return await this.ojoiApplicationApi.getPrice(params) + return await this.ojoiApplicationApiWithAuth(auth).getPrice(params) } catch (error) { - this.logger.error('Failed to get price', { + this.logger.warn('Failed to get price', { applicationId: params.id, error, category: LOG_CATEGORY, @@ -122,23 +139,43 @@ export class OfficialJournalOfIcelandApplicationClientService { } async getPresignedUrl( params: GetPresignedUrlRequest, + auth: Auth, ): Promise { - return await this.ojoiApplicationApi.getPresignedUrl(params) + return await this.ojoiApplicationApiWithAuth(auth).getPresignedUrl(params) } async addApplicationAttachment( params: AddApplicationAttachmentRequest, + auth: Auth, ): Promise { - await this.ojoiApplicationApi.addApplicationAttachment(params) + try { + await this.ojoiApplicationApiWithAuth(auth).addApplicationAttachment( + params, + ) + } catch (error) { + this.logger.warn('Failed to add application attachment', { + category: LOG_CATEGORY, + applicationId: params.id, + }) + throw error + } } - async getApplicationAttachments(params: GetApplicationAttachmentsRequest) { - return this.ojoiApplicationApi.getApplicationAttachments(params) + async getApplicationAttachments( + params: GetApplicationAttachmentsRequest, + auth: Auth, + ) { + return this.ojoiApplicationApiWithAuth(auth).getApplicationAttachments( + params, + ) } async deleteApplicationAttachment( params: DeleteApplicationAttachmentRequest, + auth: Auth, ) { - await this.ojoiApplicationApi.deleteApplicationAttachment(params) + await this.ojoiApplicationApiWithAuth(auth).deleteApplicationAttachment( + params, + ) } } From bced7f921d3589fb276b00e0ba792edbebe837a9 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:28:22 +0000 Subject: [PATCH 097/173] fix(service-portal): owner UI updates + paper signees (#16067) * fix(service-portal): UI updates + paper signees * add constituency functionality * paper signees functionality --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/hooks/graphql/mutations.ts | 21 ++ .../src/hooks/graphql/queries.ts | 8 + .../signature-collection/src/hooks/index.ts | 17 ++ .../signature-collection/src/lib/constants.ts | 10 - .../signature-collection/src/lib/messages.ts | 113 +++++++---- .../OwnerView/AddConstituency/index.tsx | 69 +++++-- .../ViewList/Signees/PaperSignees.tsx | 188 ++++++++++++++++++ .../{Signees.tsx => Signees/index.tsx} | 68 ++++--- .../OwnerView/ViewList/index.tsx | 38 +--- .../screens/Parliamentary/OwnerView/index.tsx | 124 +++++++++++- .../OwnerView}/CancelCollection/index.tsx | 18 +- .../screens/Presidential/OwnerView/index.tsx | 2 +- .../src/screens/shared/SignedList/index.tsx | 18 +- .../src/screens/shared/SigneeView/index.tsx | 3 +- 14 files changed, 538 insertions(+), 159 deletions(-) create mode 100644 libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx rename libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/{Signees.tsx => Signees/index.tsx} (69%) rename libs/service-portal/signature-collection/src/screens/{shared => Presidential/OwnerView}/CancelCollection/index.tsx (83%) diff --git a/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts b/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts index dd9bb068e793..8fb53c33cfb7 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts @@ -18,3 +18,24 @@ export const unSignList = gql` } } ` +export const addConstituency = gql` + mutation SignatureCollectionAddAreas( + $inputAdd: SignatureCollectionAddListsInput! + ) { + signatureCollectionAddAreas(input: $inputAdd) { + success + reasons + } + } +` + +export const uploadPaperSignature = gql` + mutation SignatureCollectionUploadPaperSignature( + $input: SignatureCollectionUploadPaperSignatureInput! + ) { + signatureCollectionUploadPaperSignature(input: $input) { + success + reasons + } + } +` diff --git a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts index 55a7b6a24cb9..45d50fdbe81c 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts @@ -45,6 +45,7 @@ export const GetListSignatures = gql` isDigital valid created + pageNumber } } ` @@ -70,6 +71,7 @@ export const GetSignedList = gql` collectionId canUnsign slug + signedDate } } ` @@ -155,3 +157,9 @@ export const GetCurrentCollection = gql` } } ` + +export const GetCanSign = gql` + query Query($input: SignatureCollectionCanSignInput!) { + signatureCollectionCanSign(input: $input) + } +` diff --git a/libs/service-portal/signature-collection/src/hooks/index.ts b/libs/service-portal/signature-collection/src/hooks/index.ts index d8246c12763c..9c3800136b8e 100644 --- a/libs/service-portal/signature-collection/src/hooks/index.ts +++ b/libs/service-portal/signature-collection/src/hooks/index.ts @@ -7,6 +7,7 @@ import { GetSignedList, GetListsForOwner, GetCurrentCollection, + GetCanSign, } from './graphql/queries' import { SignatureCollectionListBase, @@ -147,3 +148,19 @@ export const useGetCurrentCollection = () => { refetchCurrentCollection, } } + +export const useGetCanSign = (signeeId: string, isValidId: boolean) => { + const { data: getCanSignData, loading: loadingCanSign } = useQuery( + GetCanSign, + { + variables: { + input: { + signeeNationalId: signeeId, + }, + }, + skip: !signeeId || signeeId.length !== 10 || !isValidId, + }, + ) + const canSign = getCanSignData?.signatureCollectionCanSign ?? false + return { canSign, loadingCanSign } +} diff --git a/libs/service-portal/signature-collection/src/lib/constants.ts b/libs/service-portal/signature-collection/src/lib/constants.ts index 04eb57fdee3a..8e28b87d3fe3 100644 --- a/libs/service-portal/signature-collection/src/lib/constants.ts +++ b/libs/service-portal/signature-collection/src/lib/constants.ts @@ -2,13 +2,3 @@ export const CollectionType = { Presidential: 'Forsetakosningar', Parliamentary: 'Alþingiskosningar', } - -// will be fetched later on -export const constituencies = [ - 'Norðvesturkjördæmi', - 'Norðausturkjördæmi', - 'Suðurkjördæmi', - 'Suðvesturkjördæmi', - 'Reykjavíkurkjördæmi suður', - 'Reykjavíkurkjördæmi norður', -] diff --git a/libs/service-portal/signature-collection/src/lib/messages.ts b/libs/service-portal/signature-collection/src/lib/messages.ts index 7cad88479236..0518d12283d8 100644 --- a/libs/service-portal/signature-collection/src/lib/messages.ts +++ b/libs/service-portal/signature-collection/src/lib/messages.ts @@ -51,7 +51,7 @@ export const m = defineMessages({ }, copyLinkDescription: { id: 'sp.signatureCollection:copyLinkDescription', - defaultMessage: 'Hér getur þú afritað hlekk á þitt framboð til að deila.', + defaultMessage: 'Hér getur þú afritað hlekk á þitt framboð til að deila', description: '', }, copyLinkSuccess: { @@ -99,6 +99,11 @@ export const m = defineMessages({ defaultMessage: 'Meðmæli lesin inn', description: '', }, + digitalSignature: { + id: 'sp.signatureCollection:digitalSignature', + defaultMessage: 'Skrifað undir: ', + description: '', + }, signatureIsInvalid: { id: 'sp.signatureCollection:signatureIsInvalid', defaultMessage: 'Ógilt meðmæli', @@ -181,7 +186,12 @@ export const m = defineMessages({ }, cancelCollectionModalConfirmButton: { id: 'sp.signatureCollection:modalConfirmButton', - defaultMessage: 'Já, hætta við söfnun meðmæla', + defaultMessage: 'Já, hætta við', + description: '', + }, + cancelCollectionModalCancelButton: { + id: 'sp.signatureCollection:cancelCollectionModalCancelButton', + defaultMessage: 'Nei, hætta við', description: '', }, cancelCollectionModalToastError: { @@ -256,6 +266,61 @@ export const m = defineMessages({ defaultMessage: 'Heimilisfang', description: '', }, + paperSigneesHeader: { + id: 'sp.signatureCollection:paperSigneesHeader', + defaultMessage: 'Skrá meðmæli af blaði', + description: '', + }, + paperSigneesClearButton: { + id: 'sp.signatureCollection:paperSigneesClearButton', + defaultMessage: 'Hreinsa', + description: '', + }, + paperNumber: { + id: 'sp.signatureCollection:paperNumber', + defaultMessage: 'Blaðsíðunúmer', + description: '', + }, + paperSigneeName: { + id: 'sp.signatureCollection:paperSigneeName', + defaultMessage: 'Nafn meðmælanda', + description: '', + }, + signPaperSigneeButton: { + id: 'sp.signatureCollection:signPaperSigneeButton', + defaultMessage: 'Skrá meðmæli á lista', + description: '', + }, + paperSigneeTypoTitle: { + id: 'sp.signatureCollection:paperSigneeTypoTitle', + defaultMessage: 'Kennitala ekki á réttu formi', + description: '', + }, + paperSigneeTypoMessage: { + id: 'sp.signatureCollection:paperSigneeTypoMessage', + defaultMessage: 'Vinsamlegast athugið kennitöluna og reynið aftur', + description: '', + }, + paperSigneeCantSignTitle: { + id: 'sp.signatureCollection:paperSigneeCantSignTitle', + defaultMessage: 'Ekki er hægt að skrá meðmæli', + description: '', + }, + paperSigneeCantSignMessage: { + id: 'sp.signatureCollection:paperSigneeCantSign', + defaultMessage: 'Kennitala uppfyllir ekki skilyrði fyrir að skrá meðmæli', + description: '', + }, + paperSigneeSuccess: { + id: 'sp.signatureCollection:paperSigneeSuccess', + defaultMessage: 'Meðmæli skráð', + description: '', + }, + paperSigneeError: { + id: 'sp.signatureCollection:paperSigneeError', + defaultMessage: 'Ekki tókst að skrá meðmæli', + description: '', + }, /* Parliamentary */ parliamentaryElectionsTitle: { @@ -279,16 +344,6 @@ export const m = defineMessages({ defaultMessage: 'Þjóðskrá Íslands hefur umsjón með gögnum um meðmælasöfnun.', }, - managers: { - id: 'sp.signatureCollection:managers', - defaultMessage: 'Ábyrgðaraðilar', - description: '', - }, - addManager: { - id: 'sp.signatureCollection:addManager', - defaultMessage: 'Bæta við ábyrgðaraðila', - description: '', - }, supervisors: { id: 'sp.signatureCollection:supervisors', defaultMessage: 'Umsjónaraðilar', @@ -299,11 +354,6 @@ export const m = defineMessages({ defaultMessage: 'Bæta við', description: '', }, - addSupervisor: { - id: 'sp.signatureCollection:addSupervisor', - defaultMessage: 'Bæta við umsjónaraðila', - description: '', - }, personName: { id: 'sp.signatureCollection:personName', defaultMessage: 'Nafn', @@ -340,31 +390,14 @@ export const m = defineMessages({ 'Veldu viðeigandi kjördæmi sem þú vilt stofna meðmælendasöfnun í.', description: '', }, - addConstituencyAlertInfo: { - id: 'sp.signatureCollection:addConstituencyAlertInfo', - defaultMessage: - 'Athugið að skrá þarf viðeigandi ábyrgðar-/umsjónaraðila á yfirlitssíðu fyrir ný kjördæmi.', - description: '', - }, - deleteManager: { - id: 'sp.signatureCollection:deleteManager', - defaultMessage: 'Eyða umsjónaraðila', - description: '', - }, - deleteManagerDescription: { - id: 'sp.signatureCollection:deleteManagerDescription', - defaultMessage: - 'Þú ert að fara að taka Nafna Nafnason af lista yfir umsjónaraðila. Ertu viss um að þú viljir halda áfram?', - description: '', - }, - delete: { - id: 'sp.signatureCollection:delete', - defaultMessage: 'Eyða', + addConstituencySuccess: { + id: 'sp.signatureCollection:addConstituencySuccess', + defaultMessage: 'Kjördæmi bætt við', description: '', }, - save: { - id: 'sp.signatureCollection:save', - defaultMessage: 'Vista', + addConstituencyError: { + id: 'sp.signatureCollection:addConstituencyError', + defaultMessage: 'Ekki tókst að bæta við kjördæmi', description: '', }, }) diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx index 4820dcef6996..c6119aa3ed74 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx @@ -1,26 +1,63 @@ import { useState } from 'react' -import { Box, Button, Text, Checkbox } from '@island.is/island-ui/core' +import { Box, Button, Text, Checkbox, toast } from '@island.is/island-ui/core' import { Modal } from '@island.is/service-portal/core' import { useLocale } from '@island.is/localization' import { m } from '../../../../lib/messages' -import { constituencies } from '../../../../lib/constants' -import { SignatureCollectionList } from '@island.is/api/schema' +import { + SignatureCollection, + SignatureCollectionArea, + SignatureCollectionList, +} from '@island.is/api/schema' +import { addConstituency } from '../../../../hooks/graphql/mutations' +import { useMutation } from '@apollo/client' const AddConstituencyModal = ({ lists, + collection, + candidateId, + refetch, }: { lists: SignatureCollectionList[] + collection: SignatureCollection + candidateId: string + refetch: () => void }) => { const { formatMessage } = useLocale() - const listTitles = lists.map((l) => l.title) - const filteredConstituencies = constituencies.filter( - (c) => !listTitles.some((title) => title.includes(c)), - ) + const currentConstituencies = lists.map( + (l) => l.area, + ) as SignatureCollectionArea[] + const filteredConstituencies = collection.areas.filter( + (cc) => !currentConstituencies.some((c) => cc.name === c.name), + ) as SignatureCollectionArea[] + const [modalIsOpen, setModalIsOpen] = useState(false) const [selectedConstituencies, setSelectedConstituencies] = useState< string[] >([]) + const [addNewConstituency, { loading }] = useMutation(addConstituency, { + onCompleted: () => { + setModalIsOpen(false) + refetch() + toast.success(formatMessage(m.addConstituencySuccess)) + }, + onError: () => { + toast.error(formatMessage(m.addConstituencyError)) + }, + }) + + const onAddConstituency = async () => { + addNewConstituency({ + variables: { + inputAdd: { + collectionId: collection?.id, + areaIds: selectedConstituencies, + candidateId: candidateId, + }, + }, + }) + } + return ( + diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx new file mode 100644 index 000000000000..f101e7bd3d80 --- /dev/null +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx @@ -0,0 +1,188 @@ +import { + Box, + Text, + Button, + GridRow, + GridColumn, + GridContainer, + AlertMessage, + Input, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { useIdentityQuery } from '@island.is/service-portal/graphql' +import * as nationalId from 'kennitala' +import { useEffect, useState } from 'react' +import { InputController } from '@island.is/shared/form-fields' +import { useForm } from 'react-hook-form' +import { m } from '../../../../../lib/messages' +import { useGetCanSign } from '../../../../../hooks' +import { useMutation } from '@apollo/client' +import { uploadPaperSignature } from '../../../../../hooks/graphql/mutations' +import { toast } from 'react-toastify' + +export const PaperSignees = ({ + listId, + refetchSignees, +}: { + listId: string + refetchSignees: () => void +}) => { + useNamespaces('sp.signatureCollection') + const { formatMessage } = useLocale() + const { control, reset } = useForm() + + const [nationalIdInput, setNationalIdInput] = useState('') + const [nationalIdTypo, setNationalIdTypo] = useState(false) + const [page, setPage] = useState('') + const [name, setName] = useState('') + + /* identity & canSign fetching logic */ + const { data, loading } = useIdentityQuery({ + variables: { input: { nationalId: nationalIdInput } }, + skip: nationalIdInput.length !== 10 || !nationalId.isValid(nationalIdInput), + onCompleted: (data) => setName(data.identity?.name || ''), + }) + const { canSign, loadingCanSign } = useGetCanSign( + nationalIdInput, + nationalId.isValid(nationalIdInput), + ) + + useEffect(() => { + if (nationalIdInput.length === 10) { + setNationalIdTypo( + !nationalId.isValid(nationalIdInput) || + (!loading && !data?.identity?.name), + ) + } else { + setName('') + setNationalIdTypo(false) + } + }, [nationalIdInput, loading, data]) + + /* upload paper signature logic */ + const [upload, { loading: uploadingPaperSignature }] = useMutation( + uploadPaperSignature, + { + variables: { + input: { + listId: listId, + nationalId: nationalIdInput, + pageNumber: Number(page), + }, + }, + onCompleted: () => { + toast.success(formatMessage(m.paperSigneeSuccess)) + refetchSignees() + }, + onError: () => { + toast.error(formatMessage(m.paperSigneeError)) + }, + }, + ) + + const onClearForm = () => { + reset() // resets nationalId field + setNationalIdTypo(false) + setName('') + } + + return ( + + + + {formatMessage(m.paperSigneesHeader)} + + + + + + + + + + + { + setNationalIdInput(e.target.value.replace(/\W/g, '')) + }} + error={nationalIdTypo ? ' ' : undefined} + loading={loading || loadingCanSign} + icon={canSign ? 'checkmark' : undefined} + /> + + + setPage(e.target.value)} + /> + + + + + + + + + + + + + {nationalIdTypo && ( + + + + )} + {name && !loadingCanSign && !canSign && ( + + + + )} + + ) +} diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx similarity index 69% rename from libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx rename to libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx index 49031cc603aa..581ffb77b6be 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx @@ -4,28 +4,28 @@ import { Table as T, Pagination, FilterInput, + Icon, } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' -import { m } from '../../../../lib/messages' +import { m } from '../../../../../lib/messages' import format from 'date-fns/format' import { useEffect, useState } from 'react' -import { useGetListSignees } from '../../../../hooks' -import { useLocation } from 'react-router-dom' +import { useGetListSignees } from '../../../../../hooks' +import { useParams } from 'react-router-dom' import { format as formatNationalId } from 'kennitala' -import { SkeletonTable } from '../../../../skeletons' +import { SkeletonTable } from '../../../../../skeletons' import { SignatureCollectionSignature as Signature } from '@island.is/api/schema' +import { PaperSignees } from './PaperSignees' +import sortBy from 'lodash/sortBy' const Signees = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() - const { pathname } = useLocation() - const listId = pathname.replace( - '/min-gogn/listar/althingis-medmaelasofnun/', - '', - ) + const { id } = useParams() as { id: string } const [searchTerm, setSearchTerm] = useState('') - const { listSignees, loadingSignees } = useGetListSignees(listId) + const { listSignees, loadingSignees, refetchListSignees } = + useGetListSignees(id) const [signees, setSignees] = useState(listSignees) const [page, setPage] = useState(1) @@ -33,7 +33,11 @@ const Signees = () => { useEffect(() => { if (!loadingSignees && listSignees.length) { - setSignees(listSignees) + setSignees( + sortBy(listSignees, (item) => { + return item.created + }).reverse(), + ) } }, [listSignees]) @@ -56,30 +60,25 @@ const Signees = () => { return ( {formatMessage(m.signeesHeader)} - - - setSearchTerm(v)} - placeholder={formatMessage(m.searchInListPlaceholder)} - backgroundColor="white" - /> - + + setSearchTerm(v)} + placeholder={formatMessage(m.searchInListPlaceholder)} + backgroundColor="white" + /> {!loadingSignees ? ( signees.length > 0 ? ( - + {formatMessage(m.signeeDate)} {formatMessage(m.signeeName)} {formatMessage(m.signeeNationalId)} + @@ -88,7 +87,7 @@ const Signees = () => { .map((s: Signature) => { return ( - + {format(new Date(), 'dd.MM.yyyy')} @@ -97,13 +96,25 @@ const Signees = () => { {formatNationalId(s.signee.nationalId)} + + {!s.isDigital && ( + + + {s.pageNumber} + + )} + ) })} - + { ) : ( )} + ) } diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx index b99c8b73b533..89bf289b13fb 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx @@ -1,17 +1,15 @@ -import { Box, Button, Stack, Text, toast } from '@island.is/island-ui/core' +import { Box, Stack, Text } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' import { m } from '../../../../lib/messages' import { useParams } from 'react-router-dom' import { useGetSignatureList } from '../../../../hooks' import format from 'date-fns/format' import Signees from './Signees' -import CancelCollection from '../../../shared/CancelCollection' -import copyToClipboard from 'copy-to-clipboard' const ViewList = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() - const { id } = useParams() + const { id } = useParams() as { id: string } const { listInfo, loadingList } = useGetSignatureList(id || '') return ( @@ -54,39 +52,7 @@ const ViewList = () => { )} - - - - {formatMessage(m.copyLinkDescription)} - - - - - - )} diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx index faa2ec4c2528..659882231e88 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx @@ -5,16 +5,27 @@ import { Text, Table as T, Tooltip, + DialogPrompt, + Tag, + Icon, + toast, + Button, } from '@island.is/island-ui/core' import { useNavigate } from 'react-router-dom' import { SignatureCollectionPaths } from '../../../lib/paths' import { useLocale } from '@island.is/localization' import { m } from '../../../lib/messages' import AddConstituency from './AddConstituency' -import { SignatureCollectionList } from '@island.is/api/schema' +import { + SignatureCollectionList, + SignatureCollectionSuccess, +} from '@island.is/api/schema' import { OwnerParliamentarySkeleton } from '../../../skeletons' import { useGetListsForOwner } from '../../../hooks' import { SignatureCollection } from '@island.is/api/schema' +import { useMutation } from '@apollo/client' +import { cancelCollectionMutation } from '../../../hooks/graphql/mutations' +import copyToClipboard from 'copy-to-clipboard' const OwnerView = ({ currentCollection, @@ -23,10 +34,33 @@ const OwnerView = ({ }) => { const navigate = useNavigate() const { formatMessage } = useLocale() - const { listsForOwner, loadingOwnerLists } = useGetListsForOwner( - currentCollection?.id || '', + const { listsForOwner, loadingOwnerLists, refetchListsForOwner } = + useGetListsForOwner(currentCollection?.id || '') + + const [cancelCollection] = useMutation( + cancelCollectionMutation, + { + onCompleted: () => { + toast.success(formatMessage(m.cancelCollectionModalToastSuccess)) + refetchListsForOwner() + }, + onError: () => { + toast.error(formatMessage(m.cancelCollectionModalToastError)) + }, + }, ) + const onCancelCollection = (listId: string) => { + cancelCollection({ + variables: { + input: { + collectionId: currentCollection?.id ?? '', + listIds: listId, + }, + }, + }) + } + return ( @@ -39,11 +73,15 @@ const OwnerView = ({ color="blue400" /> - {/* If the number of lists is equal to 6, it means that - lists have been created in all of the constituencies */} - {listsForOwner.length < 6 && ( - - )} + {!loadingOwnerLists && + listsForOwner?.length < currentCollection?.areas.length && ( + + )} {loadingOwnerLists ? ( @@ -54,12 +92,13 @@ const OwnerView = ({ ( + + + + + + } + onConfirm={() => { + onCancelCollection(list.id) + }} + buttonTextConfirm={formatMessage( + m.cancelCollectionModalConfirmButton, + )} + buttonPropsConfirm={{ + variant: 'primary', + colorScheme: 'destructive', + }} + buttonTextCancel={formatMessage( + m.cancelCollectionModalCancelButton, + )} + /> + ), + }} /> )) @@ -110,6 +186,36 @@ const OwnerView = ({ + + + {formatMessage(m.copyLinkDescription)} + + + + + ) } diff --git a/libs/service-portal/signature-collection/src/screens/shared/CancelCollection/index.tsx b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/CancelCollection/index.tsx similarity index 83% rename from libs/service-portal/signature-collection/src/screens/shared/CancelCollection/index.tsx rename to libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/CancelCollection/index.tsx index 08b0cc27a325..8e11940926fd 100644 --- a/libs/service-portal/signature-collection/src/screens/shared/CancelCollection/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/CancelCollection/index.tsx @@ -1,31 +1,23 @@ import { Box, Button, Text, toast } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' -import { m } from '../../../lib/messages' +import { m } from '../../../../lib/messages' import { Modal } from '@island.is/service-portal/core' import { useState } from 'react' -import { useGetCurrentCollection } from '../../../hooks' +import { useGetCurrentCollection } from '../../../../hooks' import { useMutation } from '@apollo/client' -import { cancelCollectionMutation } from '../../../hooks/graphql/mutations' -import { - SignatureCollectionCancelListsInput, - SignatureCollectionSuccess, -} from '@island.is/api/schema' +import { cancelCollectionMutation } from '../../../../hooks/graphql/mutations' +import { SignatureCollectionSuccess } from '@island.is/api/schema' -const CancelCollection = ({ listId }: { listId?: string }) => { +const CancelCollection = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() const [modalIsOpen, setModalIsOpen] = useState(false) const { currentCollection } = useGetCurrentCollection() - const input = {} as SignatureCollectionCancelListsInput - if (listId && !currentCollection?.isPresidential) { - input.listIds = [listId] - } const [cancelCollection, { loading }] = useMutation(cancelCollectionMutation, { variables: { input: { - ...input, collectionId: currentCollection?.id ?? '', }, }, diff --git a/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx index d159e7170c02..397e3776bbe4 100644 --- a/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx @@ -17,7 +17,7 @@ import { useAuth } from '@island.is/auth/react' import copyToClipboard from 'copy-to-clipboard' import { SignatureCollection } from '@island.is/api/schema' import SignedList from '../../shared/SignedList' -import CancelCollection from '../../shared/CancelCollection' +import CancelCollection from './CancelCollection' const OwnerView = ({ currentCollection, diff --git a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx index 4cf324e865ab..df7b95016cf0 100644 --- a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx @@ -70,12 +70,8 @@ const SignedList = ({ return ( Date: Fri, 20 Sep 2024 12:25:17 +0000 Subject: [PATCH 098/173] chore(j-s): Add ability to choose if there are civil claims in a case (#16091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add radio buttons for hasCivilClaims choice and make decisions based on that * Remove files and reset civilDemands * Updates indictment screen validation --------- Co-authored-by: Guðjón Guðjónsson Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../IndictmentCaseFilesList.strings.ts | 5 ++ .../IndictmentCaseFilesList.tsx | 5 +- .../CaseFiles/CaseFiles.strings.ts | 2 +- .../Indictments/CaseFiles/CaseFiles.tsx | 58 +++++++----- .../Indictments/Indictment/Indictment.tsx | 78 ++++++++-------- .../Indictments/Processing/Processing.tsx | 88 ++++++++++++++++++- .../Processing/processing.strings.ts | 18 ++++ .../utils/hooks/useS3Upload/useS3Upload.ts | 4 +- .../judicial-system/web/src/utils/validate.ts | 14 ++- 9 files changed, 199 insertions(+), 73 deletions(-) diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts index 134cae5bdca9..7e93be0a3dfc 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts @@ -28,4 +28,9 @@ export const strings = defineMessages({ description: 'Notaður sem titill á innsend gögn hluta á dómskjalaskjá í ákærum.', }, + civilClaimsTitle: { + id: 'judicial.system.core:indictment_case_files_list.civil_claims_title', + defaultMessage: 'Einkaréttarkröfur', + description: 'Notaður sem titill á dómskjalaskjá í ákærum.', + }, }) diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index 10d1b3b71023..f28a239cf977 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -230,14 +230,15 @@ const IndictmentCaseFilesList: FC = ({ )} ) : null} - {civilClaims && + {workingCase.hasCivilClaims && + civilClaims && civilClaims.length > 0 && (isDistrictCourtUser(user) || isProsecutionUser(user) || isDefenceUser(user)) && ( - {formatMessage(caseFiles.civilClaimSection)} + {formatMessage(strings.civilClaimsTitle)} { onRetry={(file) => handleRetry(file, updateUploadFile)} /> - + @@ -172,29 +177,34 @@ const CaseFiles = () => { onRetry={(file) => handleRetry(file, updateUploadFile)} /> - - - file.category === CaseFileCategory.CIVIL_CLAIM, - )} - accept={Object.values(fileExtensionWhitelist)} - header={formatMessage(strings.caseFiles.inputFieldLabel)} - buttonLabel={formatMessage(strings.caseFiles.buttonLabel)} - onChange={(files) => - handleUpload( - addUploadFiles(files, { - category: CaseFileCategory.CIVIL_CLAIM, - }), - updateUploadFile, - ) - } - onRemove={(file) => handleRemove(file, removeUploadFile)} - onRetry={(file) => handleRetry(file, updateUploadFile)} - /> - + {workingCase.hasCivilClaims && ( + + + file.category === CaseFileCategory.CIVIL_CLAIM, + )} + accept={Object.values(fileExtensionWhitelist)} + header={formatMessage(strings.caseFiles.inputFieldLabel)} + buttonLabel={formatMessage(strings.caseFiles.buttonLabel)} + onChange={(files) => + handleUpload( + addUploadFiles(files, { + category: CaseFileCategory.CIVIL_CLAIM, + }), + updateUploadFile, + ) + } + onRemove={(file) => handleRemove(file, removeUploadFile)} + onRetry={(file) => handleRetry(file, updateUploadFile)} + /> + + )} {isTrafficViolationCaseCheck && ( { /> - - - - - removeTabsValidateAndSet( - 'civilDemands', - event.target.value, - ['empty'], - setWorkingCase, - civilDemandsErrorMessage, - setCivilDemandsErrorMessage, - ) - } - onBlur={(event) => - validateAndSendToServer( - 'civilDemands', - event.target.value, - ['empty'], - workingCase, - updateCase, - setCivilDemandsErrorMessage, - ) - } - textarea - autoComplete="off" - required - rows={7} - autoExpand={{ on: true, maxHeight: 300 }} - /> - - + {workingCase.hasCivilClaims && ( + + + + + removeTabsValidateAndSet( + 'civilDemands', + event.target.value, + ['empty'], + setWorkingCase, + civilDemandsErrorMessage, + setCivilDemandsErrorMessage, + ) + } + onBlur={(event) => + validateAndSendToServer( + 'civilDemands', + event.target.value, + ['empty'], + workingCase, + updateCase, + setCivilDemandsErrorMessage, + ) + } + textarea + autoComplete="off" + required + rows={7} + autoExpand={{ on: true, maxHeight: 300 }} + /> + + + )} { isCaseUpToDate, refreshCase, } = useContext(FormContext) - const { updateCase, transitionCase } = useCase() + const { updateCase, transitionCase, setAndSendCaseToServer } = useCase() + const { handleRemove } = useS3Upload(workingCase.id) const { formatMessage } = useIntl() const { updateDefendant, updateDefendantState } = useDefendants() const router = useRouter() const isTrafficViolationCaseCheck = isTrafficViolationCase(workingCase) + const [hasCivilClaimsChoice, setHasCivilClaimsChoice] = useState() + const initialize = useCallback(async () => { if (!workingCase.court) { await updateCase(workingCase.id, { @@ -96,6 +101,36 @@ const Processing: FC = () => { [updateDefendantState, setWorkingCase, workingCase.id, updateDefendant], ) + const handleHasCivilClaimsChange = async (hasCivilClaims: boolean) => { + setHasCivilClaimsChoice(hasCivilClaims) + + setAndSendCaseToServer( + [{ hasCivilClaims, force: true }], + workingCase, + setWorkingCase, + ) + + if (hasCivilClaims === false) { + const civilClaims = workingCase.caseFiles?.filter( + (caseFile) => caseFile.category === CaseFileCategory.CIVIL_CLAIM, + ) + + if (!civilClaims) { + return + } + + setAndSendCaseToServer( + [{ civilDemands: null, force: true }], + workingCase, + setWorkingCase, + ) + + for (const civilClaim of civilClaims) { + handleRemove(civilClaim as UploadFile) + } + } + } + return ( { ))} )} - + + + + + + + handleHasCivilClaimsChange(true)} + checked={ + hasCivilClaimsChoice === true || + (hasCivilClaimsChoice === undefined && + workingCase.hasCivilClaims === true) + } + /> + + + handleHasCivilClaimsChange(false)} + checked={ + hasCivilClaimsChoice === false || + (hasCivilClaimsChoice === undefined && + workingCase.hasCivilClaims === false) + } + /> + + + + { ) const handleRemove = useCallback( - async (file: TUploadFile, callback: (file: TUploadFile) => void) => { + async (file: TUploadFile, callback?: (file: TUploadFile) => void) => { try { if (file.id) { const { data } = await remove(file.id) @@ -421,7 +421,7 @@ const useS3Upload = (caseId: string) => { throw new Error('Failed to delete file') } - callback(file) + callback && callback(file) } } catch { toast.error(formatMessage(strings.removeFailed)) diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts index 003fa9471208..dae538e76314 100644 --- a/apps/judicial-system/web/src/utils/validate.ts +++ b/apps/judicial-system/web/src/utils/validate.ts @@ -267,15 +267,25 @@ export const isProcessingStepValidIndictments = ( return validate([[defendant.defendantPlea, ['empty']]]).isValid }) + const hasCivilClaimSelected = + workingCase.hasCivilClaims !== null && + workingCase.hasCivilClaims !== undefined + return Boolean( - workingCase.prosecutor && workingCase.court && defendantsAreValid(), + workingCase.prosecutor && + workingCase.court && + hasCivilClaimSelected && + defendantsAreValid(), ) } export const isTrafficViolationStepValidIndictments = ( workingCase: Case, ): boolean => { - return Boolean(workingCase.demands && workingCase.civilDemands) + return Boolean( + workingCase.demands && + (!workingCase.hasCivilClaims || workingCase.civilDemands), + ) } export const isPoliceDemandsStepValidRC = (workingCase: Case): boolean => { From 5a55fcf0a2fb091a360e11ce370a14330bd1cc51 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:14:10 +0000 Subject: [PATCH 099/173] feat(web): Add default header for fjarsysla rikisins (#16099) --- .../components/Organization/Wrapper/OrganizationWrapper.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 0ee977d92b69..438ff2286c17 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -387,7 +387,9 @@ export const OrganizationHeader: React.FC< case 'landing_page': return null case 'fjarsysla-rikisins': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 23 Sep 2024 08:23:09 +0000 Subject: [PATCH 100/173] fix(register-new-machine): fixing mobile (#16104) --- .../src/fields/AboutMachine/index.tsx | 8 ++++---- .../register-new-machine/src/fields/MachineType/index.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx index 6eee5336c886..b75a10f65199 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx @@ -122,8 +122,8 @@ export const AboutMachine: FC> = ( return ( - - + + > = ( } /> - + > = ( - + { diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx index 63d02c50394f..1ecb1eaa1819 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx @@ -193,8 +193,8 @@ export const MachineType: FC> = ( {formatMessage(machine.labels.machineType.inputTitle)} - - + + Date: Mon, 23 Sep 2024 09:17:32 +0000 Subject: [PATCH 101/173] fix(id-card): null check add (#16100) --- .../templates/id-card/src/utils/getChosenApplicant.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/application/templates/id-card/src/utils/getChosenApplicant.ts b/libs/application/templates/id-card/src/utils/getChosenApplicant.ts index b70232c00d37..ed38dc30ce40 100644 --- a/libs/application/templates/id-card/src/utils/getChosenApplicant.ts +++ b/libs/application/templates/id-card/src/utils/getChosenApplicant.ts @@ -25,7 +25,8 @@ export const getChosenApplicant = ( [], ) as Array - if (applicantIdentity?.nationalId === nationalId) { + //this nationalId null check is only because conditions are rendered before applicant has been chosen + if (!nationalId || applicantIdentity?.nationalId === nationalId) { return { name: applicantIdentity?.fullName, isApplicant: true, From ff9e6fca8ee6909971e39d23d1b4cd527346d745 Mon Sep 17 00:00:00 2001 From: helgifr Date: Mon, 23 Sep 2024 10:18:33 +0000 Subject: [PATCH 102/173] fix(parental-leave): Parental leave slider now gets correct value in onChangeEnd function (#16105) Co-authored-by: hfhelgason Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/components/Slider/Slider.tsx | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/libs/application/ui-components/src/components/Slider/Slider.tsx b/libs/application/ui-components/src/components/Slider/Slider.tsx index 839dab5a47f1..42df0dc0e209 100644 --- a/libs/application/ui-components/src/components/Slider/Slider.tsx +++ b/libs/application/ui-components/src/components/Slider/Slider.tsx @@ -109,10 +109,19 @@ const Slider = ({ const convertDeltaToIndex = (delta: number) => { const currentX = x + delta - - dragX.current = Math.max(min * sizePerCell, Math.min(size.width, currentX)) - - return roundByNum(dragX.current / sizePerCell, step) + const roundedMin = toFixedNumber(min, 1, 10) + dragX.current = Math.max(0, Math.min(size.width, currentX)) + // Get value to display in slider. + // Get max if more or equal to max, get min if less or equal to min and then show rest with only one decimal point. + const index = + dragX.current / sizePerCell + min >= max + ? max + : dragX.current / sizePerCell + min <= min + ? min + : roundByNum(dragX.current / sizePerCell, step) === 0 + ? min + : roundByNum(dragX.current / sizePerCell, step) + roundedMin + return index } useEffect(() => { @@ -146,19 +155,7 @@ const Slider = ({ const dragBind = useDrag({ onDragMove(deltaX: number) { - const currentX = x + deltaX - const roundedMin = toFixedNumber(min, 1, 10) - dragX.current = Math.max(0, Math.min(size.width, currentX)) - // Get value to display in slider. - // Get max if more or equal to max, get min if less or equal to min and then show rest with only one decimal point. - const index = - dragX.current / sizePerCell + min >= max - ? max - : dragX.current / sizePerCell + min <= min - ? min - : roundByNum(dragX.current / sizePerCell, step) === 0 - ? min - : roundByNum(dragX.current / sizePerCell, step) + roundedMin + const index = convertDeltaToIndex(deltaX) if (onChange && index !== indexRef.current) { onChange(index) @@ -183,7 +180,6 @@ const Slider = ({ dragX.current = undefined if (onChangeEnd) { const index = convertDeltaToIndex(deltaX) - onChangeEnd?.(index) } setIsDragging(false) From c5b064acd3b42cd1fe0cad85d05ae29a6509da89 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:29:13 +0000 Subject: [PATCH 103/173] feat(web): Add default header for HSU organization (#16107) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Organization/Wrapper/OrganizationWrapper.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 438ff2286c17..85bae312c8ee 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -333,7 +333,15 @@ export const OrganizationHeader: React.FC< /> ) case 'hsu': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 23 Sep 2024 10:53:24 +0000 Subject: [PATCH 104/173] fix(hid-application): Remove old condition (#16109) --- .../src/forms/HealthInsuranceDeclarationForm.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts b/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts index ed46fa830f95..801ff456b246 100644 --- a/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts +++ b/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts @@ -302,8 +302,6 @@ export const HealthInsuranceDeclarationForm: Form = buildForm({ ], }), ], - condition: (answers: FormValue) => - !!(answers.hasSpouse || answers.hasChildren), }), buildSection({ id: 'residencySectionTourist', From f4ab138ccb1127b9f1b001efbd937d27bb34c219 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:03:32 +0000 Subject: [PATCH 105/173] fix(service-portal): signature collection paper signees tweak (#16108) * fix(service-portal): signature collection paper signees tweak * module name tweak sp --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/service-portal/signature-collection/src/module.tsx | 2 +- .../Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/service-portal/signature-collection/src/module.tsx b/libs/service-portal/signature-collection/src/module.tsx index 50bc7c53217f..0474999b3b3d 100644 --- a/libs/service-portal/signature-collection/src/module.tsx +++ b/libs/service-portal/signature-collection/src/module.tsx @@ -37,7 +37,7 @@ export const signatureCollectionModule: PortalModule = { element: , }, { - name: m.signatureCollectionPresidentialLists, + name: m.signatureCollectionParliamentaryLists, path: SignatureCollectionPaths.ViewParliamentaryList, enabled: userInfo.scopes.includes(ApiScope.signatureCollection), key: 'ParliamentaryLists', diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx index f101e7bd3d80..877cf5b5bd8f 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx @@ -126,7 +126,7 @@ export const PaperSignees = ({ }} error={nationalIdTypo ? ' ' : undefined} loading={loading || loadingCanSign} - icon={canSign ? 'checkmark' : undefined} + icon={name && canSign ? 'checkmark' : undefined} /> @@ -156,7 +156,7 @@ export const PaperSignees = ({ + ) + + return ( + + navigate(-1)} /> +
+ +
+ + +
+ {fromIdentity?.name && ( + + )} + + + + + {fromIdentityQueryLoading ? ( + + ) : fromIdentity?.name ? ( + { + setFromNationalId('') + setFromIdentity(null) + if (defaultFromNationalId) { + setSearchParams( + (params) => { + params.delete('fromNationalId') + return params + }, + { replace: true }, + ) + } + + setTimeout(() => { + if (fromInputRef.current) { + fromInputRef.current.focus() + } + }, 0) + }} + /> + ) : null} +
+
+ +
+ {toIdentity?.name && ( + + )} + + + + {toIdentityQueryLoading ? ( + + ) : toIdentity?.name ? ( + { + setToNationalId('') + setToIdentity(null) + setTimeout(() => { + if (toInputRef.current) { + toInputRef.current.focus() + } + }, 0) + }} + /> + ) : null} +
+
+ + + + )} + + + + + + navigate(DelegationAdminPaths.Root)} + divider={false} + confirmLabel={formatMessage(m.create)} + showShadow={showShadow} + confirmIcon="arrowForward" + /> + + {actionData?.globalError && ( + + + + )} +
+
+
+ + { + setIsConfirmed(false) + setShowConfirmModal(false) + }} + onConfirm={() => { + submit(formRef.current) + setShowConfirmModal(false) + }} + /> +
+ ) +} + +export default CreateDelegationScreen diff --git a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx index 2bfcabf4fece..257ee945faa1 100644 --- a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx +++ b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx @@ -1,30 +1,71 @@ -import { Box, Stack, Tabs } from '@island.is/island-ui/core' +import { + Button, + GridColumn, + GridRow, + Stack, + Tabs, +} from '@island.is/island-ui/core' import { BackButton } from '@island.is/portals/admin/core' import { useLocale } from '@island.is/localization' import { useLoaderData, useNavigate } from 'react-router-dom' import { DelegationAdminResult } from './DelegationAdmin.loader' import { DelegationAdminPaths } from '../../lib/paths' -import { IntroHeader } from '@island.is/portals/core' +import { formatNationalId, IntroHeader } from '@island.is/portals/core' import { m } from '../../lib/messages' import React from 'react' import DelegationList from '../../components/DelegationList' import { AuthCustomDelegation } from '@island.is/api/schema' import { DelegationsEmptyState } from '@island.is/portals/shared-modules/delegations' +import { useAuth } from '@island.is/auth/react' +import { AdminPortalScope } from '@island.is/auth/scopes' +import { maskString } from '@island.is/shared/utils' const DelegationAdminScreen = () => { const { formatMessage } = useLocale() const navigate = useNavigate() const delegationAdmin = useLoaderData() as DelegationAdminResult + const { userInfo } = useAuth() + + const hasAdminAccess = userInfo?.scopes.includes( + AdminPortalScope.delegationSystemAdmin, + ) return ( navigate(DelegationAdminPaths.Root)} /> - - - + + + + + {hasAdminAccess && ( + + + + )} + { const [focused, setFocused] = useState(false) @@ -14,6 +22,12 @@ const Root = () => { const { formatMessage } = useLocale() const { isSubmitting, isLoading } = useSubmitting() const [error, setError] = useState({ hasError: false, message: '' }) + const navigate = useNavigate() + const { userInfo } = useAuth() + + const hasAdminAccess = userInfo?.scopes.includes( + AdminPortalScope.delegationSystemAdmin, + ) useEffect(() => { if (actionData?.errors) { @@ -31,39 +45,52 @@ const Root = () => { const onFocus = () => setFocused(true) const onBlur = () => setFocused(false) + return ( <> - - - -
- - setSearchInput(event.target.value), - onBlur, - onFocus, - placeholder: formatMessage(formatMessage(m.search)), - colored: true, - }} - buttonProps={{ - type: 'submit', - disabled: searchInput.length === 0, - }} - hasError={error.hasError} - errorMessage={error.hasError ? error.message : undefined} + + + - -
- +
+ {hasAdminAccess && ( + + + + )} + +
+ setSearchInput(event.target.value), + onBlur, + onFocus, + placeholder: formatMessage(formatMessage(m.search)), + colored: true, + }} + buttonProps={{ + type: 'submit', + disabled: searchInput.length === 0, + }} + hasError={error.hasError} + errorMessage={error.hasError ? error.message : undefined} + /> + +
+
) diff --git a/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx b/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx index 2208eb5cf368..7be4aa190ccc 100644 --- a/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx +++ b/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx @@ -20,6 +20,7 @@ type DelegationsFormFooterProps = { confirmIcon?: IconType confirmButtonColorScheme?: 'destructive' | 'default' showShadow?: boolean + divider?: boolean containerPaddingBottom?: ResponsiveSpace } @@ -31,6 +32,7 @@ export const DelegationsFormFooter = ({ confirmIcon, confirmButtonColorScheme = 'default', showShadow = true, + divider = true, containerPaddingBottom = 4, ...rest }: DelegationsFormFooterProps) => { @@ -39,9 +41,11 @@ export const DelegationsFormFooter = ({ return (
-
- -
+ {divider && ( +
+ +
+ )} Date: Mon, 23 Sep 2024 15:20:28 +0000 Subject: [PATCH 110/173] fix(native-app): use correct locale in applications query (#16115) * fix: use correct locale in applications query * fix: add margin for empty state in applications module * fix: show progress bar for applications with 0 of x steps finished --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/screens/applications/applications.tsx | 4 +- .../components/applications-preview.tsx | 2 +- .../src/screens/home/applications-module.tsx | 39 ++++++++++++------- .../app/src/ui/lib/card/status-card.tsx | 6 ++- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/native/app/src/screens/applications/applications.tsx b/apps/native/app/src/screens/applications/applications.tsx index 33b459f40811..42e157caf749 100644 --- a/apps/native/app/src/screens/applications/applications.tsx +++ b/apps/native/app/src/screens/applications/applications.tsx @@ -104,8 +104,10 @@ export const ApplicationsScreen: NavigationFunctionComponent = ({ const [hiddenContent, setHiddenContent] = useState(isIos) const { locale } = usePreferencesStore() + const queryLocale = locale === 'is-IS' ? 'is' : 'en' + const applicationsRes = useListApplicationsQuery({ - variables: { locale: locale === 'is-US' ? 'is' : 'en' }, + variables: { locale: queryLocale }, }) const applications = useMemo( diff --git a/apps/native/app/src/screens/applications/components/applications-preview.tsx b/apps/native/app/src/screens/applications/components/applications-preview.tsx index cec7b6fd35c5..38d3948e7ca3 100644 --- a/apps/native/app/src/screens/applications/components/applications-preview.tsx +++ b/apps/native/app/src/screens/applications/components/applications-preview.tsx @@ -83,7 +83,7 @@ export const ApplicationsPreview = ({ }, )} progressContainerWidth={ - slider ? screenWidth - theme.spacing[2] * 6 : undefined + slider && count > 1 ? screenWidth - theme.spacing[2] * 6 : undefined } description={ type !== 'incomplete' diff --git a/apps/native/app/src/screens/home/applications-module.tsx b/apps/native/app/src/screens/home/applications-module.tsx index 076b53ec768e..0b0210b7cdae 100644 --- a/apps/native/app/src/screens/home/applications-module.tsx +++ b/apps/native/app/src/screens/home/applications-module.tsx @@ -1,7 +1,8 @@ import { EmptyCard, StatusCardSkeleton } from '@ui' import React from 'react' import { useIntl } from 'react-intl' -import { Image, SafeAreaView } from 'react-native' +import styled from 'styled-components' +import { Image, SafeAreaView, View } from 'react-native' import { ApolloError } from '@apollo/client' import leJobss3 from '../../assets/illustrations/le-jobs-s3.png' @@ -20,6 +21,10 @@ interface ApplicationsModuleProps { componentId: string } +const Wrapper = styled(View)` + margin-horizontal: ${({ theme }) => theme.spacing[2]}px; +` + const validateApplicationsInitialData = ({ data, loading, @@ -50,23 +55,27 @@ const ApplicationsModule = React.memo( return ( {loading && !data ? ( - + + + ) : ( <> {count === 0 && ( - - } - link={null} - /> + + + } + link={null} + /> + )} {count !== 0 && ( @@ -128,9 +130,9 @@ export function StatusCard({ {!!description && {description}} - {!!progress && ( + {!hideProgress && ( Date: Mon, 23 Sep 2024 21:20:10 +0000 Subject: [PATCH 111/173] feat(ids-admin): Delegation-Delegation-Type (#16068) * created delegation-delegation-type.model.ts and updated findAllScopesTo in delegation-scope.service.ts * fix broken tests * tests for findAllScopesTo * added validTo to delegationDelegationType * set general mandate as type in ids select account prompt * Get general mandate to delegations-to on service-portal * remove duplicate case * small refactor * chore: nx format:write update dirty files * fix tests after merge with main * move general mandate tests to new file * add zendesk validation * fix comments from PR * fix pr comment * chore: nx format:write update dirty files * fix pr comment --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- ...ersonal-representative.controller.spec.ts} | 5 +- .../test/delegations.controller.spec.ts | 278 ++++++++++++++++++ .../actorDelegations.controller.spec.ts | 93 +++++- apps/services/auth/public-api/test/setup.ts | 1 + ...240916092301-delegation-delegation-type.js | 55 ++++ .../seeders/local/012-general-mandate.js | 44 +++ libs/auth-api-lib/src/index.ts | 1 + .../admin/delegation-admin-custom.service.ts | 3 +- .../delegations/delegation-scope.service.ts | 67 ++++- .../delegations-incoming-custom.service.ts | 120 +++++++- .../delegations-incoming.service.ts | 18 +- .../src/lib/delegations/delegations.module.ts | 2 + .../delegation-delegation-type.model.ts | 67 +++++ .../models/delegation-type.model.ts | 4 + .../delegations/models/delegation.model.ts | 14 +- .../admin/delegation-admin/project.json | 6 + .../src/components/access/AccessCard.tsx | 3 + .../delegations/src/lib/messages.ts | 4 + .../src/fixtures/delegation.fixture.ts | 2 +- 19 files changed, 762 insertions(+), 25 deletions(-) rename apps/services/auth/ids-api/src/app/delegations/{delegations.controller.spec.ts => delegations-personal-representative.controller.spec.ts} (99%) create mode 100644 apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts create mode 100644 libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js create mode 100644 libs/auth-api-lib/seeders/local/012-general-mandate.js create mode 100644 libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts diff --git a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts similarity index 99% rename from apps/services/auth/ids-api/src/app/delegations/delegations.controller.spec.ts rename to apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts index e5f5875ce638..f0050b0d9e9e 100644 --- a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts @@ -6,6 +6,7 @@ import request from 'supertest' import { ApiScope, ApiScopeDelegationType, + Client, DelegationProviderModel, DelegationsIndexService, DelegationTypeModel, @@ -52,12 +53,13 @@ import { personalRepresentativeType, } from '../../../test/stubs/personalRepresentativeStubs' -describe('DelegationsController', () => { +describe('Personal Representative DelegationsController', () => { describe('Given a user is authenticated', () => { let app: TestApp let factory: FixtureFactory let server: request.SuperTest let apiScopeModel: typeof ApiScope + let clientModel: typeof Client let prScopePermission: typeof PersonalRepresentativeScopePermission let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType let prModel: typeof PersonalRepresentative @@ -143,6 +145,7 @@ describe('DelegationsController', () => { delegationProviderModel = app.get( getModelToken(DelegationProviderModel), ) + clientModel = app.get(getModelToken(Client)) nationalRegistryApi = app.get(NationalRegistryClientService) delegationIndexService = app.get(DelegationsIndexService) factory = new FixtureFactory(app) diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts new file mode 100644 index 000000000000..4abd4987bd31 --- /dev/null +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts @@ -0,0 +1,278 @@ +import { getModelToken } from '@nestjs/sequelize' +import request from 'supertest' +import { uuid } from 'uuidv4' +import addDays from 'date-fns/addDays' + +import { + ApiScope, + ApiScopeDelegationType, + Delegation, + DelegationDelegationType, + DelegationProviderModel, + DelegationScope, + DelegationTypeModel, + Domain, +} from '@island.is/auth-api-lib' +import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { + createClient, + createDomain, + FixtureFactory, +} from '@island.is/services/auth/testing' +import { + AuthDelegationProvider, + AuthDelegationType, +} from '@island.is/shared/types' +import { + createCurrentUser, + createNationalRegistryUser, +} from '@island.is/testing/fixtures' +import { TestApp } from '@island.is/testing/nest' + +import { defaultScopes, setupWithAuth } from '../../../../test/setup' +import { getFakeNationalId } from '../../../../test/stubs/genericStubs' + +describe('DelegationsController', () => { + describe('Given a user is authenticated', () => { + let app: TestApp + let factory: FixtureFactory + let server: request.SuperTest + + let apiScopeModel: typeof ApiScope + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + let delegationDelegationTypeModel: typeof DelegationDelegationType + let delegationModel: typeof Delegation + let delegationTypeModel: typeof DelegationTypeModel + let nationalRegistryApi: NationalRegistryClientService + let delegationProviderModel: typeof DelegationProviderModel + let delegationScopesModel: typeof DelegationScope + + const client = createClient({ + clientId: '@island.is/webapp', + }) + + const scopeValid1 = 'scope/valid1' + const scopeValid2 = 'scope/valid2' + const scopeValid1and2 = 'scope/valid1and2' + const scopeUnactiveType = 'scope/unactiveType' + const scopeOutdated = 'scope/outdated' + const disabledScope = 'disabledScope' + + client.allowedScopes = Object.values([ + scopeValid1, + scopeValid2, + scopeValid1and2, + scopeUnactiveType, + scopeOutdated, + disabledScope, + ]).map((s) => ({ + clientId: client.clientId, + scopeName: s, + })) + + const userNationalId = getFakeNationalId() + + const user = createCurrentUser({ + nationalId: userNationalId, + scope: [defaultScopes.testUserHasAccess.name], + client: client.clientId, + }) + + const domain = createDomain() + + beforeAll(async () => { + app = await setupWithAuth({ + user, + }) + server = request(app.getHttpServer()) + + const domainModel = app.get(getModelToken(Domain)) + await domainModel.create(domain) + + apiScopeModel = app.get(getModelToken(ApiScope)) + + apiScopeDelegationTypeModel = app.get( + getModelToken(ApiScopeDelegationType), + ) + delegationTypeModel = app.get( + getModelToken(DelegationTypeModel), + ) + delegationProviderModel = app.get( + getModelToken(DelegationProviderModel), + ) + delegationScopesModel = app.get( + getModelToken(DelegationScope), + ) + delegationModel = app.get(getModelToken(Delegation)) + delegationDelegationTypeModel = app.get( + getModelToken(DelegationDelegationType), + ) + nationalRegistryApi = app.get(NationalRegistryClientService) + factory = new FixtureFactory(app) + }) + + afterAll(async () => { + await app.cleanUp() + }) + + describe('GET with general mandate delegation type', () => { + const representeeNationalId = getFakeNationalId() + let nationalRegistryApiSpy: jest.SpyInstance + const scopeNames = [ + 'api-scope/generalMandate1', + 'api-scope/generalMandate2', + 'api-scope/generalMandate3', + ] + + beforeAll(async () => { + client.supportedDelegationTypes = [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + ] + await factory.createClient(client) + + const delegations = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: representeeNationalId, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationProviderModel.create({ + id: AuthDelegationProvider.Custom, + name: 'Custom', + description: 'Custom', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegations.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + await apiScopeModel.bulkCreate( + scopeNames.map((name) => ({ + name, + domainName: domain.name, + enabled: true, + description: `${name}: description`, + displayName: `${name}: display name`, + })), + ) + + // set 2 of 3 scopes to have general mandate delegation type + await apiScopeDelegationTypeModel.bulkCreate([ + { + apiScopeName: scopeNames[0], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[1], + delegationType: AuthDelegationType.GeneralMandate, + }, + ]) + + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return user ?? null + }) + }) + + afterAll(async () => { + await app.cleanUp() + nationalRegistryApiSpy.mockClear() + }) + + it('should return mergedDelegationDTO with the generalMandate', async () => { + const response = await server.get('/v2/delegations') + + expect(response.status).toEqual(200) + expect(response.body).toHaveLength(1) + }) + + it('should get all general mandate scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + expect(response.status).toEqual(200) + expect(response.body).toEqual([scopeNames[0], scopeNames[1]]) + }) + + it('should only return valid general mandates', async () => { + const newNationalId = getFakeNationalId() + const newDelegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: newNationalId, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationDelegationTypeModel.create({ + delegationId: newDelegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: addDays(new Date(), -2), + }) + + const response = await server.get('/delegations/scopes').query({ + fromNationalId: newNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + expect(response.status).toEqual(200) + expect(response.body).toEqual([]) + }) + + it('should return all general mandate scopes and other preset scopes', async () => { + const newDelegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: representeeNationalId, + domainName: domain.name, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationTypeModel.create({ + id: AuthDelegationType.Custom, + name: 'custom', + description: 'custom', + providerId: AuthDelegationProvider.Custom, + }) + + await delegationScopesModel.create({ + id: uuid(), + delegationId: newDelegation.id, + scopeName: scopeNames[2], + // set valid from as yesterday and valid to as tomorrow + validFrom: addDays(new Date(), -1), + validTo: addDays(new Date(), 1), + }) + + await apiScopeDelegationTypeModel.create({ + apiScopeName: scopeNames[2], + delegationType: AuthDelegationType.LegalGuardian, + }) + + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + ], + }) + + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(scopeNames)) + expect(response.body).toHaveLength(scopeNames.length) + }) + }) + }) +}) diff --git a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts index c4183a2a61d7..b178739555d5 100644 --- a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts +++ b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts @@ -3,10 +3,9 @@ import times from 'lodash/times' import request from 'supertest' import { - ApiScope, - Client, ClientDelegationType, Delegation, + DelegationDelegationType, DelegationDTO, DelegationDTOMapper, DelegationProviderModel, @@ -153,6 +152,7 @@ describe('ActorDelegationsController', () => { let app: TestApp let server: request.SuperTest let delegationModel: typeof Delegation + let delegationDelegationTypeModel: typeof DelegationDelegationType let clientDelegationTypeModel: typeof ClientDelegationType let nationalRegistryApi: NationalRegistryClientService @@ -174,6 +174,9 @@ describe('ActorDelegationsController', () => { clientDelegationTypeModel = app.get( getModelToken(ClientDelegationType), ) + delegationDelegationTypeModel = app.get( + getModelToken(DelegationDelegationType), + ) nationalRegistryApi = app.get(NationalRegistryClientService) }) @@ -293,20 +296,96 @@ describe('ActorDelegationsController', () => { ) }) - it('should return custom delegations when the delegationTypes filter has custom type', async () => { + it('should return custom delegations and general mandate when the delegationTypes filter has both types and delegation exists for both', async () => { // Arrange + const delegation = createDelegation({ + fromNationalId: nationalRegistryUser.nationalId, + toNationalId: user.nationalId, + scopes: [], + }) + + await delegationModel.create(delegation) + + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + await createDelegationModels(delegationModel, [ mockDelegations.incomingWithOtherDomain, ]) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(2) + expect( + res.body + .map((d: MergedDelegationDTO) => d.types) + .flat() + .sort(), + ).toEqual( + [AuthDelegationType.Custom, AuthDelegationType.GeneralMandate].sort(), + ) + }) + + it('should return a merged object with both Custom and GeneralMandate types', async () => { + // Arrange + const delegation = createDelegation({ + fromNationalId: + mockDelegations.incomingWithOtherDomain.fromNationalId, + toNationalId: user.nationalId, + domainName: null, + scopes: [], + }) + + await delegationModel.create(delegation) + + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect( + res.body + .map((d: MergedDelegationDTO) => d.types) + .flat() + .sort(), + ).toEqual( + [AuthDelegationType.Custom, AuthDelegationType.GeneralMandate].sort(), + ) + }) + + it('should return only delegations related to the provided otherUser national id', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incoming, + ]) const expectedModel = await findExpectedMergedDelegationModels( delegationModel, - mockDelegations.incomingWithOtherDomain.id, + mockDelegations.incoming.id, [Scopes[0].name], ) // Act const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${mockDelegations.incoming.fromNationalId}`, ) // Assert @@ -321,7 +400,7 @@ describe('ActorDelegationsController', () => { ) }) - it('should return only delegations related to the provided otherUser national id', async () => { + it('should return only delegations related to the provided otherUser national id without the general mandate since there is none', async () => { // Arrange await createDelegationModels(delegationModel, [ mockDelegations.incoming, @@ -334,7 +413,7 @@ describe('ActorDelegationsController', () => { // Act const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${mockDelegations.incoming.fromNationalId}`, + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes${AuthDelegationType.GeneralMandate}&otherUser=${mockDelegations.incoming.fromNationalId}`, ) // Assert diff --git a/apps/services/auth/public-api/test/setup.ts b/apps/services/auth/public-api/test/setup.ts index c56ac554a07d..5cd632add54c 100644 --- a/apps/services/auth/public-api/test/setup.ts +++ b/apps/services/auth/public-api/test/setup.ts @@ -72,6 +72,7 @@ export const delegationTypes = [ AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.GeneralMandate, ] export const ScopeGroups: ScopeGroupSetupOptions[] = [ diff --git a/libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js b/libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js new file mode 100644 index 000000000000..453370fae9c1 --- /dev/null +++ b/libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js @@ -0,0 +1,55 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('delegation_delegation_type', { + delegation_id: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'delegation', // Table name + key: 'id', + }, + onDelete: 'CASCADE', + primaryKey: true, + }, + delegation_type_id: { + type: Sequelize.STRING, + allowNull: false, + references: { + model: 'delegation_type', // Table name + key: 'id', + }, + onDelete: 'CASCADE', + primaryKey: true, + }, + valid_to: { + type: Sequelize.DATE, + allowNull: true, + }, + created: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('now'), + }, + modified: { + type: Sequelize.DATE, + }, + }) + + await queryInterface.addConstraint('delegation_delegation_type', { + fields: ['delegation_id', 'delegation_type_id'], + type: 'unique', + name: 'unique_delegation_delegation_type', + }) + }, + + async down(queryInterface) { + await queryInterface.removeConstraint( + 'delegation_delegation_type', + 'unique_delegation_delegation_type', + ) + + await queryInterface.dropTable('delegation_delegation_type') + }, +} diff --git a/libs/auth-api-lib/seeders/local/012-general-mandate.js b/libs/auth-api-lib/seeders/local/012-general-mandate.js new file mode 100644 index 000000000000..7f97c92309fc --- /dev/null +++ b/libs/auth-api-lib/seeders/local/012-general-mandate.js @@ -0,0 +1,44 @@ +const { uuid } = require('uuidv4') + +module.exports = { + up: async (queryInterface) => { + const transaction = await queryInterface.sequelize.transaction() + const id = uuid() + + try { + await queryInterface.bulkInsert( + 'delegation', + [ + { + id, + to_national_id: '0101302399', + from_national_id: '0101307789', + from_display_name: 'Gervimaður útlönd', + to_name: 'Gervimaður Færeyjar', + }, + ], + { transaction }, + ) + + await queryInterface.bulkInsert( + 'delegation_delegation_type', + [ + { + delegation_id: id, + delegation_type_id: 'GeneralMandate', + }, + ], + { transaction }, + ) + + await transaction.commit() + } catch (err) { + await transaction.rollback() + console.log(err) + throw err + } + }, + down: async () => { + // Do nothing + }, +} diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts index d39fa234af6f..68842edcc489 100644 --- a/libs/auth-api-lib/src/index.ts +++ b/libs/auth-api-lib/src/index.ts @@ -52,6 +52,7 @@ export * from './lib/delegations/models/delegation-scope.model' export * from './lib/delegations/models/delegation-index.model' export * from './lib/delegations/models/delegation-index-meta.model' export * from './lib/delegations/models/delegation-type.model' +export * from './lib/delegations/models/delegation-delegation-type.model' export * from './lib/delegations/models/delegation-provider.model' export * from './lib/delegations/DelegationConfig' export * from './lib/delegations/utils/scopes' diff --git a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts index 2a55384d2623..f5f878a88e15 100644 --- a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts @@ -97,10 +97,9 @@ export class DelegationAdminCustomService { } async deleteDelegation(user: User, delegationId: string): Promise { - // TODO: Check if delegation has a ReferenceId and throw error if it does not. const delegation = await this.delegationModel.findByPk(delegationId) - if (!delegation) { + if (!delegation || !delegation.referenceId) { throw new NoContentException() } diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts index bda3d0856fcc..bb6f5a332112 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts @@ -6,7 +6,10 @@ import startOfDay from 'date-fns/startOfDay' import { Op, Transaction } from 'sequelize' import { uuid } from 'uuidv4' -import { AuthDelegationProvider } from '@island.is/shared/types' +import { + AuthDelegationProvider, + AuthDelegationType, +} from '@island.is/shared/types' import { PersonalRepresentativeDelegationTypeModel } from '../personal-representative/models/personal-representative-delegation-type.model' import { PersonalRepresentative } from '../personal-representative/models/personal-representative.model' @@ -21,6 +24,7 @@ import { DelegationTypeModel } from './models/delegation-type.model' import { Delegation } from './models/delegation.model' import type { User } from '@island.is/auth-nest-tools' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' @Injectable() export class DelegationScopeService { @@ -31,6 +35,8 @@ export class DelegationScopeService { private apiScopeModel: typeof ApiScope, @InjectModel(IdentityResource) private identityResourceModel: typeof IdentityResource, + @InjectModel(Delegation) + private delegationModel: typeof Delegation, @Inject(DelegationConfig.KEY) private delegationConfig: ConfigType, private delegationProviderService: DelegationProviderService, @@ -121,7 +127,7 @@ export class DelegationScopeService { toNationalId: string, fromNationalId: string, ): Promise { - const today = startOfDay(new Date()) + const today = new Date() return this.delegationScopeModel.findAll({ where: { @@ -161,6 +167,51 @@ export class DelegationScopeService { }) } + private async findValidGeneralMandateScopesTo( + toNationalId: string, + fromNationalId: string, + ): Promise { + const today = startOfDay(new Date()) + + const delegations = await this.delegationModel.findAll({ + where: { + toNationalId, + fromNationalId, + }, + include: [ + { + model: DelegationDelegationType, + required: true, + where: { + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: { + [Op.or]: [{ [Op.is]: undefined }, { [Op.gte]: today }], + }, + }, + }, + ], + }) + + if (delegations.length === 0) return [] + + return this.apiScopeModel + .findAll({ + attributes: ['name'], + where: { + enabled: true, + }, + include: [ + { + model: ApiScopeDelegationType, + where: { + delegationType: AuthDelegationType.GeneralMandate, + }, + }, + ], + }) + .then((apiScopes) => apiScopes.map((apiScope) => apiScope.name)) + } + private async findAllNationalRegistryScopes(): Promise { const apiScopes = await this.apiScopeModel.findAll({ include: [ @@ -296,7 +347,7 @@ export class DelegationScopeService { if ( providers.includes(AuthDelegationProvider.PersonalRepresentativeRegistry) - ) + ) { scopePromises.push( this.findPersonalRepresentativeRegistryScopes( user.nationalId, @@ -304,14 +355,22 @@ export class DelegationScopeService { delegationTypes, ), ) + } - if (providers.includes(AuthDelegationProvider.Custom)) + if (delegationTypes?.includes(AuthDelegationType.Custom)) { scopePromises.push( this.findValidCustomScopesTo(user.nationalId, fromNationalId).then( (delegationScopes: DelegationScope[]) => delegationScopes.map((ds) => ds.scopeName), ), ) + } + + if (delegationTypes?.includes(AuthDelegationType.GeneralMandate)) { + scopePromises.push( + this.findValidGeneralMandateScopesTo(user.nationalId, fromNationalId), + ) + } const scopeSets = await Promise.all(scopePromises) diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts index 40022c8082fc..b13ce486becc 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts @@ -3,16 +3,14 @@ import { InjectModel } from '@nestjs/sequelize' import * as kennitala from 'kennitala' import uniqBy from 'lodash/uniqBy' import { Op } from 'sequelize' +import startOfDay from 'date-fns/startOfDay' import { User } from '@island.is/auth-nest-tools' import { IndividualDto, NationalRegistryClientService, } from '@island.is/clients/national-registry-v2' -import { - CompanyExtendedInfo, - CompanyRegistryClientService, -} from '@island.is/clients/rsk/company-registry' +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' import { LOGGER_PROVIDER } from '@island.is/logging' import { AuditService } from '@island.is/nest/audit' import { AuthDelegationType } from '@island.is/shared/types' @@ -30,6 +28,7 @@ import { Delegation } from './models/delegation.model' import { DelegationValidity } from './types/delegationValidity' import { partitionWithIndex } from './utils/partitionWithIndex' import { getScopeValidityWhereClause } from './utils/scopes' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' type FindAllValidIncomingOptions = { nationalId: string @@ -84,6 +83,33 @@ export class DelegationsIncomingCustomService { }) } + async findAllValidGeneralMandate( + { nationalId }: FindAllValidIncomingOptions, + useMaster = false, + ): Promise { + const { delegations, fromNameInfo } = + await this.findAllIncomingGeneralMandates( + { + nationalId, + }, + useMaster, + ) + + return delegations.map((delegation) => { + const delegationDTO = delegation.toDTO(AuthDelegationType.GeneralMandate) + + const person = this.getPersonByNationalId( + fromNameInfo, + delegationDTO.fromNationalId, + ) + + return { + ...delegationDTO, + fromName: person?.name ?? delegationDTO.fromName ?? UNKNOWN_NAME, + } + }) + } + async findAllAvailableIncoming( user: User, clientAllowedApiScopes: ApiScopeInfo[], @@ -151,6 +177,92 @@ export class DelegationsIncomingCustomService { }) } + async findAllAvailableGeneralMandate( + user: User, + clientAllowedApiScopes: ApiScopeInfo[], + requireApiScopes: boolean, + ): Promise { + const customApiScopes = clientAllowedApiScopes.filter((s) => + s.supportedDelegationTypes?.some( + (dt) => dt.delegationType === AuthDelegationType.GeneralMandate, + ), + ) + + if (requireApiScopes && !(customApiScopes && customApiScopes.length > 0)) { + return [] + } + + const { delegations, fromNameInfo } = + await this.findAllIncomingGeneralMandates({ + nationalId: user.nationalId, + }) + + const mergedDelegationDTOs = uniqBy( + delegations.map((d) => + d.toMergedDTO([AuthDelegationType.GeneralMandate]), + ), + 'fromNationalId', + ) + + return mergedDelegationDTOs.map((d) => { + const person = this.getPersonByNationalId(fromNameInfo, d.fromNationalId) + + return { + ...d, + fromName: person?.name ?? d.fromName ?? UNKNOWN_NAME, + } as MergedDelegationDTO + }) + } + + private async findAllIncomingGeneralMandates( + { nationalId }: FindAllValidIncomingOptions, + useMaster = false, + ): Promise<{ delegations: Delegation[]; fromNameInfo: FromNameInfo[] }> { + const startOfToday = startOfDay(new Date()) + + const delegations = await this.delegationModel.findAll({ + useMaster, + where: { + toNationalId: nationalId, + }, + include: [ + { + model: DelegationDelegationType, + where: { + validTo: { + [Op.or]: { + [Op.gte]: startOfToday, + [Op.is]: null, + }, + }, + delegationTypeId: AuthDelegationType.GeneralMandate, + }, + }, + ], + }) + + // Check live status, i.e. dead or alive for delegations + const { aliveDelegations, deceasedDelegations, fromNameInfo } = + await this.getLiveStatusFromDelegations(delegations) + + if (deceasedDelegations.length > 0) { + // Delete all deceased delegations by deleting them and their scopes. + const deletePromises = deceasedDelegations.map((delegation) => + delegation.destroy(), + ) + + await Promise.all(deletePromises) + + this.auditService.audit({ + action: 'deleteDelegationsForMissingPeople', + resources: deceasedDelegations.map(({ id }) => id).filter(isDefined), + system: true, + }) + } + + return { delegations: aliveDelegations, fromNameInfo } + } + private async findAllIncoming( { nationalId, diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts index 3a162241164b..30eb53b694c3 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts @@ -102,6 +102,12 @@ export class DelegationsIncomingService { }), ) + delegationPromises.push( + this.delegationsIncomingCustomService.findAllValidGeneralMandate({ + nationalId: user.nationalId, + }), + ) + delegationPromises.push( this.delegationsIncomingRepresentativeService.findAllIncoming({ nationalId: user.nationalId, @@ -172,7 +178,7 @@ export class DelegationsIncomingService { ) } - if (providers.includes(AuthDelegationProvider.Custom)) { + if (types?.includes(AuthDelegationType.Custom)) { delegationPromises.push( this.delegationsIncomingCustomService.findAllAvailableIncoming( user, @@ -182,6 +188,16 @@ export class DelegationsIncomingService { ) } + if (types?.includes(AuthDelegationType.GeneralMandate)) { + delegationPromises.push( + this.delegationsIncomingCustomService.findAllAvailableGeneralMandate( + user, + clientAllowedApiScopes, + client.requireApiScopes, + ), + ) + } + if ( providers.includes(AuthDelegationProvider.PersonalRepresentativeRegistry) ) { diff --git a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts index 6353fdd7d211..c74aaca8a940 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts @@ -34,6 +34,7 @@ import { DelegationProviderModel } from './models/delegation-provider.model' import { DelegationProviderService } from './delegation-provider.service' import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model' import { DelegationAdminCustomService } from './admin/delegation-admin-custom.service' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' @Module({ imports: [ @@ -57,6 +58,7 @@ import { DelegationAdminCustomService } from './admin/delegation-admin-custom.se ApiScopeUserAccess, DelegationTypeModel, DelegationProviderModel, + DelegationDelegationType, ]), UserSystemNotificationModule, ], diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts new file mode 100644 index 000000000000..ba00c7fc4b3f --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts @@ -0,0 +1,67 @@ +import type { + CreationOptional, + InferAttributes, + InferCreationAttributes, +} from 'sequelize' +import { + Table, + Column, + Model, + ForeignKey, + DataType, + CreatedAt, + UpdatedAt, + BelongsTo, +} from 'sequelize-typescript' + +import { Delegation } from './delegation.model' +import { DelegationTypeModel } from './delegation-type.model' + +@Table({ + tableName: 'delegation_delegation_type', + timestamps: false, + indexes: [ + { + fields: ['delegation_id', 'delegation_type_id'], + unique: true, + }, + ], +}) +export class DelegationDelegationType extends Model< + InferAttributes, + InferCreationAttributes +> { + @ForeignKey(() => Delegation) + @Column({ + type: DataType.STRING, + primaryKey: true, + allowNull: false, + }) + delegationId!: string + + @ForeignKey(() => DelegationTypeModel) + @Column({ + type: DataType.STRING, + primaryKey: true, + allowNull: false, + }) + delegationTypeId!: string + + @Column({ + type: DataType.DATE, + allowNull: true, + }) + validTo?: Date + + @CreatedAt + readonly created!: CreationOptional + + @UpdatedAt + readonly modified?: Date + + @BelongsTo(() => Delegation) + delegation?: Delegation + + @BelongsTo(() => DelegationTypeModel) + delegationType?: DelegationTypeModel +} diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts index 51527569ba3b..36a578bce566 100644 --- a/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts @@ -24,6 +24,7 @@ import { ClientDelegationType } from '../../clients/models/client-delegation-typ import { Client } from '../../clients/models/client.model' import { ApiScopeDelegationType } from '../../resources/models/api-scope-delegation-type.model' import { ApiScope } from '../../resources/models/api-scope.model' +import { DelegationDelegationType } from './delegation-delegation-type.model' @Table({ tableName: 'delegation_type', @@ -79,6 +80,9 @@ export class DelegationTypeModel extends Model< @HasMany(() => PersonalRepresentativeDelegationTypeModel) prDelegationType?: PersonalRepresentativeDelegationTypeModel[] + @HasMany(() => DelegationDelegationType) + delegationDelegationTypes?: DelegationDelegationType[] + toDTO(): DelegationTypeDto { return { id: this.id, diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts index fcb192dc7010..41ef58402d08 100644 --- a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts @@ -24,6 +24,7 @@ import { AuthDelegationProvider, AuthDelegationType, } from '@island.is/shared/types' +import { DelegationDelegationType } from './delegation-delegation-type.model' @Table({ tableName: 'delegation', @@ -45,7 +46,7 @@ export class Delegation extends Model< primaryKey: true, allowNull: false, }) - id!: CreationOptional + id!: string @Column({ type: DataType.STRING, @@ -119,7 +120,10 @@ export class Delegation extends Model< @HasMany(() => DelegationScope, { onDelete: 'cascade' }) delegationScopes?: NonAttribute - toDTO(): DelegationDTO { + @HasMany(() => DelegationDelegationType, { onDelete: 'cascade' }) + delegationDelegationTypes?: DelegationDelegationType[] + + toDTO(type = AuthDelegationType.Custom): DelegationDTO { return { id: this.id, fromName: this.fromDisplayName, @@ -131,19 +135,19 @@ export class Delegation extends Model< ? this.delegationScopes.map((scope) => scope.toDTO()) : [], provider: AuthDelegationProvider.Custom, - type: AuthDelegationType.Custom, + type: type, domainName: this.domainName, } } - toMergedDTO(): MergedDelegationDTO { + toMergedDTO(types = [AuthDelegationType.Custom]): MergedDelegationDTO { return { fromName: this.fromDisplayName, fromNationalId: this.fromNationalId, toNationalId: this.toNationalId, toName: this.toName, validTo: this.validTo, - types: [AuthDelegationType.Custom], + types: types, scopes: this.delegationScopes ? this.delegationScopes.map((scope) => scope.toDTO()) : [], diff --git a/libs/portals/admin/delegation-admin/project.json b/libs/portals/admin/delegation-admin/project.json index 5088d4044799..dbd46bfe137c 100644 --- a/libs/portals/admin/delegation-admin/project.json +++ b/libs/portals/admin/delegation-admin/project.json @@ -15,6 +15,12 @@ "jestConfig": "libs/portals/admin/delegation-admin/jest.config.ts" } }, + "extract-strings": { + "executor": "nx:run-commands", + "options": { + "command": "yarn ts-node -P libs/localization/tsconfig.lib.json libs/localization/scripts/extract 'libs/portals/admin/delegation-admin/src/**/*.{ts,tsx}'" + } + }, "codegen/frontend-client": { "executor": "nx:run-commands", "options": { diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx index a7589e542044..ab3273fb365d 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx @@ -100,6 +100,9 @@ export const AccessCard = ({ let icon: IconType = 'people' switch (type) { + case AuthDelegationType.GeneralMandate: + label = formatMessage(m.delegationTypeGeneralMandate) + break case AuthDelegationType.LegalGuardian: label = formatMessage(m.delegationTypeLegalGuardian) break diff --git a/libs/portals/shared-modules/delegations/src/lib/messages.ts b/libs/portals/shared-modules/delegations/src/lib/messages.ts index 2ccb9eebed9b..ae37baa97455 100644 --- a/libs/portals/shared-modules/delegations/src/lib/messages.ts +++ b/libs/portals/shared-modules/delegations/src/lib/messages.ts @@ -30,6 +30,10 @@ export const m = defineMessages({ id: 'sp.access-control-delegations:delegation-type-legal-guardian', defaultMessage: 'Forsjá', }, + delegationTypeGeneralMandate: { + id: 'sp.access-control-delegations:delegation-type-general-mandate', + defaultMessage: 'Allsherjarumboð', + }, delegationTypeProcurationHolder: { id: 'sp.access-control-delegations:delegation-type-procuration-holder', defaultMessage: 'Prókúra', diff --git a/libs/services/auth/testing/src/fixtures/delegation.fixture.ts b/libs/services/auth/testing/src/fixtures/delegation.fixture.ts index 84d055729221..fe86b56faebd 100644 --- a/libs/services/auth/testing/src/fixtures/delegation.fixture.ts +++ b/libs/services/auth/testing/src/fixtures/delegation.fixture.ts @@ -16,7 +16,7 @@ export interface CreateDelegationOptions { today?: Date expired?: boolean future?: boolean - domainName?: string + domainName?: string | null } export type CreateDelegationScope = Pick< From 4e76ba99e529f3bb989a27df4d1ce2077a9850bf Mon Sep 17 00:00:00 2001 From: birkirkristmunds <142495885+birkirkristmunds@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:44:48 +0000 Subject: [PATCH 112/173] fix(skilavottord): Fix issue with numberplate count (#16120) * TS-916 Fix issue with numberplate count * TS-916 Fix code after code rabbit review * TS-916 Fix code after code rabbit review --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../CarDetailsBox2/CarDetailsBox2.tsx | 1 + .../DeregisterVehicle/Confirm/Confirm.tsx | 24 ++++++++++++++++--- .../app/modules/vehicle/vehicle.resolver.ts | 2 +- .../app/modules/vehicle/vehicle.service.ts | 3 +-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx b/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx index b76419da7cf3..546459bafcb3 100644 --- a/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx +++ b/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx @@ -33,6 +33,7 @@ interface BoxProps { vinNumber?: string outInStatus: number useStatus: string + reloadFlag: boolean // To force reload of the component to make sure the data in the parent is correct } export const CarDetailsBox2: FC> = ({ diff --git a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx index 293ac53554ed..53333b718288 100644 --- a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx +++ b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx @@ -1,7 +1,7 @@ import { useMutation, useQuery } from '@apollo/client' import gql from 'graphql-tag' import { useRouter } from 'next/router' -import React, { FC, useContext, useEffect } from 'react' +import React, { FC, useContext, useEffect, useState } from 'react' import { Box, @@ -33,7 +33,7 @@ import { Role, } from '@island.is/skilavottord-web/graphql/schema' import { useI18n } from '@island.is/skilavottord-web/i18n' -import { OutInUsage } from '@island.is/skilavottord-web/utils/consts' +import { OutInUsage, UseStatus } from '@island.is/skilavottord-web/utils/consts' import { getYear } from '@island.is/skilavottord-web/utils/dateUtils' import { FormProvider, useForm } from 'react-hook-form' @@ -100,6 +100,17 @@ const UpdateSkilavottordVehicleInfoMutation = gql` ` const Confirm: FC> = () => { + const [reloadFlag, setReloadFlag] = useState(false) + + // Update reloadFlag to trigger the child component to reload + const triggerReload = () => { + setReloadFlag(true) + } + + useEffect(() => { + triggerReload() + }, [setReloadFlag]) + const methods = useForm({ mode: 'onChange', }) @@ -184,6 +195,7 @@ const Confirm: FC> = () => { const handleConfirm = () => { let newMileage = mileageValue + let plateCount = plateCountValue if (mileageValue !== undefined) { newMileage = +mileageValue.trim().replace(/\./g, '') @@ -191,12 +203,17 @@ const Confirm: FC> = () => { newMileage = vehicle?.mileage } + // If vehicle is out of use and not using ticket, set plate count to 0 + if (outInStatus === OutInUsage.OUT && useStatus !== UseStatus.OUT_TICKET) { + plateCount = 0 + } + // Update vehicle table with latests information setVehicleRequest({ variables: { permno: vehicle?.vehicleId, mileage: newMileage, - plateCount: plateCountValue === 0 ? 0 : plateCountValue, + plateCount, plateLost: !!plateLost?.length, }, }).then(() => { @@ -274,6 +291,7 @@ const Confirm: FC> = () => { mileage={vehicle.mileage || 0} outInStatus={outInStatus} useStatus={useStatus || ''} + reloadFlag={reloadFlag} /> diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts index a5f2af14ec32..d8a1516ffda1 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts @@ -108,7 +108,7 @@ export class VehicleResolver { @CurrentUser() user: User, @Args('permno') permno: string, @Args('mileage') mileage: number, - @Args('plateCount') plateCount: number, + @Args('plateCount', { nullable: true }) plateCount: number, @Args('plateLost') plateLost: boolean, ) { return await this.vehicleService.updateVehicleInfo( diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts index 3be43b3a65b8..67729e02c3a9 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts @@ -69,7 +69,7 @@ export class VehicleService { const findVehicle = await this.findByVehicleId(permno) if (findVehicle) { findVehicle.mileage = mileage ?? 0 - findVehicle.plateCount = plateCount ?? 0 + findVehicle.plateCount = plateCount findVehicle.plateLost = plateLost await findVehicle.save() @@ -85,7 +85,6 @@ export class VehicleService { throw new Error(errorMsg) } } - async create(vehicle: VehicleModel): Promise { try { // check if vehicle is already in db From c5d69fdf1c0e3d7505fcca905e01638b33caa9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorkell=20M=C3=A1ni=20=C3=9Eorkelsson?= Date: Tue, 24 Sep 2024 09:20:49 +0000 Subject: [PATCH 113/173] fix(work-machines): last inspection date fix (#16040) * fix: init * fix: add message * feat: fix ui if no messages --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/workMachines.service.ts | 38 ++++++++++++------- .../service-portal/assets/src/lib/messages.ts | 4 ++ .../WorkMachinesDetail/WorkMachinesDetail.tsx | 6 ++- .../WorkMachinesOverview.tsx | 22 +++-------- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/libs/api/domains/work-machines/src/lib/workMachines.service.ts b/libs/api/domains/work-machines/src/lib/workMachines.service.ts index cee1ca0ffc09..1dfb2186d2ec 100644 --- a/libs/api/domains/work-machines/src/lib/workMachines.service.ts +++ b/libs/api/domains/work-machines/src/lib/workMachines.service.ts @@ -5,6 +5,7 @@ import { TechInfoItemDto, WorkMachinesClientService, } from '@island.is/clients/work-machines' +import { isDefined } from '@island.is/shared/utils' import { User } from '@island.is/auth-nest-tools' import { WorkMachine, @@ -19,6 +20,8 @@ import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' import { MachineDto } from '@island.is/clients/work-machines' import { GetMachineParentCategoryByTypeAndModelInput } from './dto/getMachineParentCategoryByTypeAndModel.input' +import isValid from 'date-fns/isValid' + @Injectable() export class WorkMachinesService { constructor( @@ -75,17 +78,22 @@ export class WorkMachinesService { ) } - const value = data.value?.map( - (v) => - ({ - ...v, - dateLastInspection: v.dateLastInspection + const workMachines: Array = + data.value + ?.map((v) => { + const inspectionDate = v.dateLastInspection ? new Date(v.dateLastInspection) - : undefined, - ownerName: v.owner, - supervisorName: v.supervisor, - } as WorkMachine), - ) as Array + : undefined + return { + ...v, + dateLastInspection: isValid(inspectionDate) + ? inspectionDate + : undefined, + ownerName: v.owner, + supervisorName: v.supervisor, + } + }) + .filter(isDefined) ?? [] const links = data.links?.length ? data.links.map((l) => { @@ -97,7 +105,7 @@ export class WorkMachinesService { : null return { - data: value, + data: workMachines, links, labels: data.labels, totalCount: data.pagination?.totalCount ?? 0, @@ -133,11 +141,13 @@ export class WorkMachinesService { }) : null + const inspectionDate = data.dateLastInspection + ? new Date(data.dateLastInspection) + : undefined + return { ...data, - dateLastInspection: data.dateLastInspection - ? new Date(data.dateLastInspection) - : undefined, + dateLastInspection: isValid(inspectionDate) ? inspectionDate : undefined, links, } } diff --git a/libs/service-portal/assets/src/lib/messages.ts b/libs/service-portal/assets/src/lib/messages.ts index 61ea965a9ebf..ef01b45773f5 100644 --- a/libs/service-portal/assets/src/lib/messages.ts +++ b/libs/service-portal/assets/src/lib/messages.ts @@ -167,6 +167,10 @@ export const messages = defineMessages({ id: 'sp.work-machines:search-placeholder', defaultMessage: 'Leita', }, + noInspection: { + id: 'sp.work-machines:no-inspection', + defaultMessage: 'Óskoðuð', + }, }) export const vehicleMessage = defineMessages({ diff --git a/libs/service-portal/assets/src/screens/WorkMachinesDetail/WorkMachinesDetail.tsx b/libs/service-portal/assets/src/screens/WorkMachinesDetail/WorkMachinesDetail.tsx index 6498ad16edac..7ffa17e2abd4 100644 --- a/libs/service-portal/assets/src/screens/WorkMachinesDetail/WorkMachinesDetail.tsx +++ b/libs/service-portal/assets/src/screens/WorkMachinesDetail/WorkMachinesDetail.tsx @@ -177,7 +177,11 @@ const WorkMachinesDetail = () => { /> { }, }) } - const generateCardText = (wm: WorkMachine): string | undefined => { - let text = wm.registrationNumber ?? undefined - const inspectionText = wm.dateLastInspection - ? `${formatMessage(vehicleMessage.lastInspection)}: ${formatDate( - wm.dateLastInspection, - )}` - : undefined - - if (wm.dateLastInspection) { - text = wm.registrationNumber - ? `${text}, ${inspectionText}` - : inspectionText - } - - return text - } return ( @@ -273,7 +257,11 @@ const WorkMachinesOverview = () => { return ( Date: Tue, 24 Sep 2024 10:29:06 +0000 Subject: [PATCH 114/173] chore(application-system): Payments updates to lifecycle on callback, UX fixes. (#15883) * fixes to swagger and updates to lifecycle on callback, screen updates * pr fixes * fix tests --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../e2e/payment/payment-callback.spec.ts | 20 ++++---- apps/application-system/api/src/openApi.ts | 1 + .../src/lib/api-domains-payment.types.ts | 26 ++++++++-- .../lib/application/application.service.ts | 1 + .../src/lib/payment-callback.controller.ts | 25 ++++++++-- libs/application/core/src/lib/messages.ts | 6 +++ .../PaymentPending/PaymentPending.tsx | 47 +++++++++++++++---- 7 files changed, 99 insertions(+), 27 deletions(-) diff --git a/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts b/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts index 4f3a44d88b0b..7352887abc88 100644 --- a/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts +++ b/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts @@ -16,18 +16,16 @@ beforeAll(async () => { describe('Application system payments callback API', () => { // Sets the payment status to paid. it(`POST /application-payment/32eee126-6b7f-4fca-b9a0-a3618b3e42bf/6b11dc9f-a694-440e-b3dd-7163b5f34815 should update payment fulfilled`, async () => { - await server + const response = await server .post( '/application-payment/32eee126-6b7f-4fca-b9a0-a3618b3e42bf/6b11dc9f-a694-440e-b3dd-7163b5f34815', ) .send({ - callback: { - receptionID: '1234567890', - chargeItemSubject: 'Very nice subject', - status: 'paid', - }, + receptionID: '123e4567-e89b-12d3-a456-426614174000', // Updated to real UUID + chargeItemSubject: 'Very nice subject', + status: 'paid', }) - .expect(201) + expect(response.status).toBe(201) }) // Fails to set the payment status to paid. @@ -37,11 +35,9 @@ describe('Application system payments callback API', () => { '/application-payment/32eee126-6b7f-4fca-b9a0-a3618b3e42bf/missing-id', ) .send({ - callback: { - receptionID: '1234567890', - chargeItemSubject: 'nice subject.. not', - status: 'paid', - }, + receptionID: '123e4567-e89b-12d3-a456-426614174000', // Updated to real UUID + chargeItemSubject: 'nice subject.. not', + status: 'paid', }) .expect(400) }) diff --git a/apps/application-system/api/src/openApi.ts b/apps/application-system/api/src/openApi.ts index 1c22ee62d9ab..fa773e20e102 100644 --- a/apps/application-system/api/src/openApi.ts +++ b/apps/application-system/api/src/openApi.ts @@ -8,5 +8,6 @@ export const openApi = new DocumentBuilder() .setVersion('1.0') .addTag('application') .addTag('payment') + .addTag('payment-callback') .addBearerAuth() .build() diff --git a/libs/api/domains/payment/src/lib/api-domains-payment.types.ts b/libs/api/domains/payment/src/lib/api-domains-payment.types.ts index c07c61cf62dd..30ea95f06dbe 100644 --- a/libs/api/domains/payment/src/lib/api-domains-payment.types.ts +++ b/libs/api/domains/payment/src/lib/api-domains-payment.types.ts @@ -1,3 +1,6 @@ +import { IsEnum, IsString } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + export interface ChargeResult { success: boolean error: Error | null @@ -14,8 +17,23 @@ export interface CallbackResult { data?: Callback } -export interface Callback { - receptionID: string - chargeItemSubject: string - status: 'paid' | 'cancelled' | 'recreated' | 'recreatedAndPaid' +export enum PaidStatus { + paid = 'paid', + cancelled = 'cancelled', + recreated = 'recreated', + recreatedAndPaid = 'recreatedAndPaid', +} + +export class Callback { + @IsString() + @ApiProperty() + readonly receptionID!: string + + @IsString() + @ApiProperty() + readonly chargeItemSubject!: string + + @IsEnum(PaidStatus) + @ApiProperty({ enum: PaidStatus }) + readonly status!: PaidStatus } diff --git a/libs/application/api/core/src/lib/application/application.service.ts b/libs/application/api/core/src/lib/application/application.service.ts index b55415927be0..194a1bf50dfe 100644 --- a/libs/application/api/core/src/lib/application/application.service.ts +++ b/libs/application/api/core/src/lib/application/application.service.ts @@ -299,6 +299,7 @@ export class ApplicationService { | 'applicantActors' | 'draftTotalSteps' | 'draftFinishedSteps' + | 'pruneAt' > >, ) { diff --git a/libs/application/api/payment/src/lib/payment-callback.controller.ts b/libs/application/api/payment/src/lib/payment-callback.controller.ts index 61505938a624..3333aaf6d1e7 100644 --- a/libs/application/api/payment/src/lib/payment-callback.controller.ts +++ b/libs/application/api/payment/src/lib/payment-callback.controller.ts @@ -1,15 +1,22 @@ import { Body, Controller, Param, Post, ParseUUIDPipe } from '@nestjs/common' -import type { Callback } from '@island.is/api/domains/payment' +import { Callback } from '@island.is/api/domains/payment' import { PaymentService } from './payment.service' +import { ApplicationService } from '@island.is/application/api/core' +import { ApiTags } from '@nestjs/swagger' +import addMonths from 'date-fns/addMonths' +@ApiTags('payment-callback') @Controller() export class PaymentCallbackController { - constructor(private readonly paymentService: PaymentService) {} + constructor( + private readonly paymentService: PaymentService, + private readonly applicationService: ApplicationService, + ) {} @Post('application-payment/:applicationId/:id') async paymentApproved( - @Param('applicationId', new ParseUUIDPipe()) applicationId: string, @Body() callback: Callback, + @Param('applicationId', new ParseUUIDPipe()) applicationId: string, @Param('id', new ParseUUIDPipe()) id: string, ): Promise { if (callback.status !== 'paid') { @@ -21,5 +28,17 @@ export class PaymentCallbackController { callback.receptionID, applicationId, ) + + const application = await this.applicationService.findOneById(applicationId) + if (application) { + const oneMonthFromNow = addMonths(new Date(), 1) + //Applications payment states are default to be pruned in 24 hours. + //If the application is paid, we want to hold on to it for longer in case we get locked in an error state. + + await this.applicationService.update(applicationId, { + ...application, + pruneAt: oneMonthFromNow, + }) + } } } diff --git a/libs/application/core/src/lib/messages.ts b/libs/application/core/src/lib/messages.ts index ff96c66b7026..27e1439cbd2b 100644 --- a/libs/application/core/src/lib/messages.ts +++ b/libs/application/core/src/lib/messages.ts @@ -421,6 +421,12 @@ export const coreErrorMessages = defineMessages({ defaultMessage: 'Sending umsóknar mistókst', description: 'Message indicating submission after payment failed', }, + paymentSubmitFailedDescription: { + id: 'application.system:core.payment.submitTitle', + defaultMessage: + 'Villa hefur komið upp við áframhaldandi vinnslu. Vinsamlegast reynið aftur síðar. Ef villa endurtekur sig vinsamlegast hafið samband við island@island.is.', + description: 'Message indicating submission after payment failed', + }, applicationSubmitFailed: { id: 'application.system:core.application.SubmitFailed', defaultMessage: 'Sending umsóknar mistókst', diff --git a/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx b/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx index 376903656e98..d4c040b47b4e 100644 --- a/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx +++ b/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx @@ -5,10 +5,15 @@ import { DefaultEvents, FieldBaseProps, } from '@island.is/application/types' -import { Box, Button, Text } from '@island.is/island-ui/core' +import { + AlertMessage, + Box, + Button, + LoadingDots, + Text, +} from '@island.is/island-ui/core' import { useSubmitApplication, usePaymentStatus, useMsg } from './hooks' import { getRedirectStatus, getRedirectUrl, isComingFromRedirect } from './util' -import { Company } from './assets' import { useSearchParams } from 'react-router-dom' export interface PaymentPendingProps { @@ -84,10 +89,27 @@ export const PaymentPending: FC< if (submitError) { return ( - {msg(coreErrorMessages.paymentSubmitFailed)} - + + + + + + ) } @@ -95,8 +117,17 @@ export const PaymentPending: FC< return ( {msg(coreMessages.paymentPollingIndicator)} - - + + ) From 52fc588336610b86bd9a41499f3dcb134c1dfec5 Mon Sep 17 00:00:00 2001 From: valurefugl <65780958+valurefugl@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:32:55 +0100 Subject: [PATCH 115/173] feat(ids-api): Use syslumenn api to verify delegation. (#16029) * Use syslumenn api to verify delegation. * Fix api error case. * Fix config. * chore: nx format:write update dirty files * Handle error in api. * Fix typo in name. * Refactor mock. * Update infra. * Update host in infra. * Add syslumenn infra to other auth apis. * Also return empty array if error. * Use post. * Fix tests. * Single delegation type. * Remove infra config from pr public. * Fix type. * Refactor error handling in check for scopes. * Openapi fix. * Refactor verification error handling. * Fix status code. * Decrease syslumenn api timeout to 3s. --------- Co-authored-by: andes-it Co-authored-by: Valur Einarsson Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../auth/admin-api/infra/auth-admin-api.ts | 8 + .../auth/admin-api/src/app/app.module.ts | 18 +- .../delegation-api/infra/delegation-api.ts | 10 +- .../auth/delegation-api/src/app/app.module.ts | 4 +- apps/services/auth/ids-api/infra/ids-api.ts | 8 + .../auth/ids-api/src/app/app.module.ts | 4 +- .../delegation-verification-result.dto.ts | 8 + .../delegation-verification.dto.ts | 19 ++ .../app/delegations/delegations.controller.ts | 29 ++- .../test/delegations-filters.spec.ts | 59 ++++- .../test/delegations-scopes.spec.ts | 10 + apps/services/auth/ids-api/test/setup.ts | 13 + .../infra/personal-representative.ts | 8 + .../src/app/app.module.ts | 26 +- .../auth/public-api/infra/auth-public-api.ts | 11 +- .../auth/public-api/src/app/app.module.ts | 10 +- charts/identity-server/values.dev.yaml | 20 ++ charts/identity-server/values.prod.yaml | 20 ++ charts/identity-server/values.staging.yaml | 20 ++ .../delegations/delegation-scope.service.ts | 67 ++++- .../delegations-incoming.service.ts | 47 +++- .../src/lib/delegations/delegations.module.ts | 38 +-- libs/clients/syslumenn/src/clientConfig.json | 229 +++++++++++++++++- .../src/lib/syslumennClient.service.ts | 127 ++++++---- 24 files changed, 698 insertions(+), 115 deletions(-) create mode 100644 apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts create mode 100644 apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index fe1fe79e2480..8e9d21d11f06 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -59,6 +59,12 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { prod: 'IS/GOV/5402696029/Skatturinn/ft-v1', }, COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG, + SYSLUMENN_HOST: { + dev: 'https://api.syslumenn.is/staging', + staging: 'https://api.syslumenn.is/staging', + prod: 'https://api.syslumenn.is', + }, + SYSLUMENN_TIMEOUT: '3000', }) .secrets({ CLIENT_SECRET_ENCRYPTION_KEY: @@ -67,6 +73,8 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET', NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', }) .xroad(Base, Client, RskProcuring) .ingress({ diff --git a/apps/services/auth/admin-api/src/app/app.module.ts b/apps/services/auth/admin-api/src/app/app.module.ts index b409548fb8a2..982ab803273f 100644 --- a/apps/services/auth/admin-api/src/app/app.module.ts +++ b/apps/services/auth/admin-api/src/app/app.module.ts @@ -8,7 +8,13 @@ import { SequelizeConfigService, } from '@island.is/auth-api-lib' import { AuthModule } from '@island.is/auth-nest-tools' +import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' +import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' +import { SyslumennClientConfig } from '@island.is/clients/syslumenn' import { AuditModule } from '@island.is/nest/audit' +import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' +import { FeatureFlagConfig } from '@island.is/nest/feature-flags' import { ProblemModule } from '@island.is/nest/problem' import { environment } from '../environments' @@ -21,16 +27,11 @@ import { ResourcesModule } from './modules/resources/resources.module' import { TranslationModule } from './modules/translation/translation.module' import { UsersModule } from './modules/users/users.module' import { ClientsModule as ClientsV2Module } from './v2/clients/clients.module' +import { DelegationAdminModule } from './v2/delegations/delegation-admin.module' +import { ProvidersModule } from './v2/providers/providers.module' +import { ScopesModule } from './v2/scopes/scopes.module' import { ClientSecretsModule } from './v2/secrets/client-secrets.module' import { TenantsModule } from './v2/tenants/tenants.module' -import { ScopesModule } from './v2/scopes/scopes.module' -import { ProvidersModule } from './v2/providers/providers.module' -import { DelegationAdminModule } from './v2/delegations/delegation-admin.module' -import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' -import { FeatureFlagConfig } from '@island.is/nest/feature-flags' -import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' -import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' -import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' @Module({ imports: [ @@ -64,6 +65,7 @@ import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' FeatureFlagConfig, XRoadConfig, IdsClientConfig, + SyslumennClientConfig, ], envFilePath: ['.env', '.env.secret'], }), diff --git a/apps/services/auth/delegation-api/infra/delegation-api.ts b/apps/services/auth/delegation-api/infra/delegation-api.ts index 1ceff205f6d3..60202a6822d8 100644 --- a/apps/services/auth/delegation-api/infra/delegation-api.ts +++ b/apps/services/auth/delegation-api/infra/delegation-api.ts @@ -1,8 +1,8 @@ import { json, + ref, service, ServiceBuilder, - ref, } from '../../../../../infra/src/dsl/dsl' import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' @@ -54,12 +54,20 @@ export const serviceSetup = (services: { prod: 'IS/GOV/5402696029/Skatturinn/ft-v1', }, COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG, + SYSLUMENN_HOST: { + dev: 'https://api.syslumenn.is/staging', + staging: 'https://api.syslumenn.is/staging', + prod: 'https://api.syslumenn.is', + }, + SYSLUMENN_TIMEOUT: '3000', }) .secrets({ IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET', NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', }) .xroad(Base, Client, RskProcuring) .readiness('/health/check') diff --git a/apps/services/auth/delegation-api/src/app/app.module.ts b/apps/services/auth/delegation-api/src/app/app.module.ts index 10b6209d5bce..b425506e3878 100644 --- a/apps/services/auth/delegation-api/src/app/app.module.ts +++ b/apps/services/auth/delegation-api/src/app/app.module.ts @@ -7,9 +7,10 @@ import { SequelizeConfigService, } from '@island.is/auth-api-lib' import { AuthModule } from '@island.is/auth-nest-tools' +import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' -import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' +import { SyslumennClientConfig } from '@island.is/clients/syslumenn' import { AuditModule } from '@island.is/nest/audit' import { ConfigModule, @@ -50,6 +51,7 @@ import { ScopesModule } from './scopes/scopes.module' CompanyRegistryConfig, XRoadConfig, DelegationApiUserSystemNotificationConfig, + SyslumennClientConfig, ], }), ], diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts index efae3c5d56d9..e72a270d0d37 100644 --- a/apps/services/auth/ids-api/infra/ids-api.ts +++ b/apps/services/auth/ids-api/infra/ids-api.ts @@ -83,6 +83,12 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => { // Origin for Android prod app 'android:apk-key-hash:EsLTUu5kaY7XPmMl2f7nbq4amu-PNzdYu3FecNf90wU', ]), + SYSLUMENN_HOST: { + dev: 'https://api.syslumenn.is/staging', + staging: 'https://api.syslumenn.is/staging', + prod: 'https://api.syslumenn.is', + }, + SYSLUMENN_TIMEOUT: '3000', }) .secrets({ IDENTITY_SERVER_CLIENT_SECRET: @@ -92,6 +98,8 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => { NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD', NATIONAL_REGISTRY_B2C_CLIENT_SECRET: '/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET', + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', }) .xroad(Base, Client, RskProcuring, NationalRegistryAuthB2C) .readiness('/health/check') diff --git a/apps/services/auth/ids-api/src/app/app.module.ts b/apps/services/auth/ids-api/src/app/app.module.ts index 4f8a3e3d670f..f4aeb431e8c8 100644 --- a/apps/services/auth/ids-api/src/app/app.module.ts +++ b/apps/services/auth/ids-api/src/app/app.module.ts @@ -11,6 +11,7 @@ import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationshi import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3' import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' +import { SyslumennClientConfig } from '@island.is/clients/syslumenn' import { UserProfileClientConfig } from '@island.is/clients/user-profile' import { AuditModule } from '@island.is/nest/audit' import { @@ -28,12 +29,12 @@ import { DelegationsModule } from './delegations/delegations.module' import { GrantsModule } from './grants/grants.module' import { LoginRestrictionsModule } from './login-restrictions/login-restrictions.module' import { NotificationsModule } from './notifications/notifications.module' +import { PasskeysModule } from './passkeys/passkeys.module' import { PermissionsModule } from './permissions/permissions.module' import { ResourcesModule } from './resources/resources.module' import { TranslationModule } from './translation/translation.module' import { UserProfileModule } from './user-profile/user-profile.module' import { UsersModule } from './users/users.module' -import { PasskeysModule } from './passkeys/passkeys.module' @Module({ imports: [ @@ -68,6 +69,7 @@ import { PasskeysModule } from './passkeys/passkeys.module' PasskeysCoreConfig, NationalRegistryV3ClientConfig, smsModuleConfig, + SyslumennClientConfig, ], }), ], diff --git a/apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts b/apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts new file mode 100644 index 000000000000..6f99d970922f --- /dev/null +++ b/apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsBoolean } from 'class-validator' + +export class DelegationVerificationResult { + @IsBoolean() + @ApiProperty() + verified!: boolean +} diff --git a/apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts b/apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts new file mode 100644 index 000000000000..aa799809529d --- /dev/null +++ b/apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsArray, IsEnum, IsString } from 'class-validator' + +import { AuthDelegationType } from '@island.is/shared/types' + +export class DelegationVerification { + @IsString() + @ApiProperty() + fromNationalId!: string + + @IsArray() + @IsEnum(AuthDelegationType, { each: true }) + @ApiProperty({ + enum: AuthDelegationType, + enumName: 'AuthDelegationType', + isArray: true, + }) + delegationTypes!: AuthDelegationType[] +} diff --git a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts b/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts index df91500afe6e..8310d931d823 100644 --- a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts +++ b/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts @@ -1,8 +1,10 @@ import { + Body, Controller, Get, Inject, ParseArrayPipe, + Post, Query, UseGuards, Version, @@ -25,11 +27,14 @@ import { ScopesGuard, } from '@island.is/auth-nest-tools' import { LOGGER_PROVIDER } from '@island.is/logging' +import { Documentation } from '@island.is/nest/swagger' import { AuthDelegationType } from '@island.is/shared/types' +import { DelegationVerificationResult } from './delegation-verification-result.dto' +import { DelegationVerification } from './delegation-verification.dto' + import type { Logger } from '@island.is/logging' import type { User } from '@island.is/auth-nest-tools' - @UseGuards(IdsUserGuard, ScopesGuard) @ApiTags('delegations') @Controller({ @@ -110,4 +115,26 @@ export class DelegationsController { delegationType, ) } + + @Scopes('@identityserver.api/authentication') + @Post('verify') + @Documentation({ + description: 'Verifies a delegation at the source.', + response: { status: 200, type: DelegationVerificationResult }, + }) + @ApiOkResponse({ type: DelegationVerificationResult }) + async verify( + @CurrentUser() user: User, + @Body() + request: DelegationVerification, + ): Promise { + const verified = + await this.delegationsIncomingService.verifyDelegationAtProvider( + user, + request.fromNationalId, + request.delegationTypes, + ) + + return { verified } + } } diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts index c6032e1def00..526bf4262a4a 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts @@ -15,7 +15,10 @@ import { import { createNationalRegistryUser } from '@island.is/testing/fixtures' import { TestApp, truncate } from '@island.is/testing/nest' -import { setupWithAuth } from '../../../../test/setup' +import { + nonExistingLegalRepresentativeNationalId, + setupWithAuth, +} from '../../../../test/setup' import { testCases } from './delegations-filters-test-cases' import { user } from './delegations-filters-types' @@ -128,4 +131,58 @@ describe('DelegationsController', () => { }) }, ) + + describe('verify', () => { + const testCase = testCases['legalRepresentative1'] + testCase.user = user + const path = '/v1/delegations/verify' + + beforeAll(async () => { + await truncate(sequelize) + + await Promise.all( + testCase.domains.map((domain) => factory.createDomain(domain)), + ) + + await factory.createClient(testCase.client) + + await Promise.all( + testCase.clientAllowedScopes.map((scope) => + factory.createClientAllowedScope(scope), + ), + ) + + await Promise.all( + testCase.apiScopes.map((scope) => factory.createApiScope(scope)), + ) + + await factory.createDelegationIndexRecord({ + fromNationalId: nonExistingLegalRepresentativeNationalId, + toNationalId: testCase.user.nationalId, + type: AuthDelegationType.LegalRepresentative, + provider: AuthDelegationProvider.DistrictCommissionersRegistry, + }) + }) + + let res: request.Response + it(`POST ${path} returns verified response`, async () => { + res = await server.post(path).send({ + fromNationalId: testCase.fromLegalRepresentative[0], + delegationTypes: [AuthDelegationType.LegalRepresentative], + }) + + expect(res.status).toEqual(200) + expect(res.body.verified).toEqual(true) + }) + + it(`POST ${path} returns non-verified response`, async () => { + res = await server.post(path).send({ + fromNationalId: nonExistingLegalRepresentativeNationalId, + delegationTypes: [AuthDelegationType.LegalRepresentative], + }) + + expect(res.status).toEqual(200) + expect(res.body.verified).toEqual(false) + }) + }) }) diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts index 31915631e076..61367667d5c2 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts @@ -22,12 +22,14 @@ const legalGuardianScopes = ['lg1', 'lg2'] const procurationHolderScopes = ['ph1', 'ph2'] const customScopes1 = ['cu1', 'cu2'] const customScopes2 = ['cu3', 'cu4'] +const legalRepresentativeScopes = ['lr1', 'lr2'] const apiScopes = [ ...legalGuardianScopes, ...procurationHolderScopes, ...customScopes1, ...customScopes2, + ...legalRepresentativeScopes, ] const fromCustom = [ @@ -48,6 +50,9 @@ const supportedDelegationTypes = (scopeName: string): AuthDelegationType[] => { if (customScopes1.includes(scopeName) || customScopes2.includes(scopeName)) { result.push(AuthDelegationType.Custom) } + if (legalRepresentativeScopes.includes(scopeName)) { + result.push(AuthDelegationType.LegalRepresentative) + } return result } @@ -98,6 +103,11 @@ const testCases: Record = { ], expected: [...legalGuardianScopes, ...identityResources], }, + '7': { + fromNationalId: createNationalId('person'), + delegationType: [AuthDelegationType.LegalRepresentative], + expected: [...legalRepresentativeScopes, ...identityResources], + }, } const user = createCurrentUser({ diff --git a/apps/services/auth/ids-api/test/setup.ts b/apps/services/auth/ids-api/test/setup.ts index c6b7ae7f9e7b..a04e722bfcc8 100644 --- a/apps/services/auth/ids-api/test/setup.ts +++ b/apps/services/auth/ids-api/test/setup.ts @@ -12,6 +12,7 @@ import { RskRelationshipsClient } from '@island.is/clients-rsk-relationships' import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' +import { SyslumennService } from '@island.is/clients/syslumenn' import { V2MeApi } from '@island.is/clients/user-profile' import { FeatureFlagService, Features } from '@island.is/nest/feature-flags' import { @@ -21,6 +22,7 @@ import { } from '@island.is/services/auth/testing' import { createCurrentUser, + createNationalId, createUniqueWords, } from '@island.is/testing/fixtures' import { @@ -67,6 +69,8 @@ export const defaultScopes: Scopes = { }, } +export const nonExistingLegalRepresentativeNationalId = createNationalId() + class MockNationalRegistryClientService implements Partial { @@ -85,6 +89,13 @@ class MockUserProfile { meUserProfileControllerFindUserProfile = jest.fn().mockResolvedValue({}) } +class MockSyslumennService { + checkIfDelegationExists = jest.fn( + (_toNationalId: string, fromNationalId: string) => + fromNationalId !== nonExistingLegalRepresentativeNationalId, + ) +} + interface SetupOptions { user: User scopes?: Scopes @@ -125,6 +136,8 @@ export const setupWithAuth = async ({ .useValue({ getIndividualRelationships: jest.fn().mockResolvedValue(null), }) + .overrideProvider(SyslumennService) + .useClass(MockSyslumennService) .overrideProvider(FeatureFlagService) .useValue({ getValue: (feature: Features) => diff --git a/apps/services/auth/personal-representative/infra/personal-representative.ts b/apps/services/auth/personal-representative/infra/personal-representative.ts index 9a54b8c4206b..bb3c79004506 100644 --- a/apps/services/auth/personal-representative/infra/personal-representative.ts +++ b/apps/services/auth/personal-representative/infra/personal-representative.ts @@ -42,10 +42,18 @@ export const serviceSetup = prod: 'IS/GOV/5402696029/Skatturinn/ft-v1', }, COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG, + SYSLUMENN_HOST: { + dev: 'https://api.syslumenn.is/staging', + staging: 'https://api.syslumenn.is/staging', + prod: 'https://api.syslumenn.is', + }, + SYSLUMENN_TIMEOUT: '3000', }) .secrets({ IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET', + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', }) .xroad(Base, Client, RskProcuring) .ingress({ diff --git a/apps/services/auth/personal-representative/src/app/app.module.ts b/apps/services/auth/personal-representative/src/app/app.module.ts index 14a44d76dc13..74d0e3f0c759 100644 --- a/apps/services/auth/personal-representative/src/app/app.module.ts +++ b/apps/services/auth/personal-representative/src/app/app.module.ts @@ -1,26 +1,29 @@ -import { RightTypesModule } from './modules/rightTypes/rightTypes.module' -import { PersonalRepresentativesModule } from './modules/personalRepresentatives/personalRepresentatives.module' -import { PersonalRepresentativeTypesModule } from './modules/personalRepresentativeTypes/personalRepresentativeTypes.module' -import { AccessLogsModule } from './modules/accessLogs/accessLogs.module' +import { Module } from '@nestjs/common' +import { SequelizeModule } from '@nestjs/sequelize' + import { DelegationConfig, SequelizeConfigService, } from '@island.is/auth-api-lib' -import { Module } from '@nestjs/common' -import { SequelizeModule } from '@nestjs/sequelize' -import { environment } from '../environments' -import { AuditModule } from '@island.is/nest/audit' import { AuthModule } from '@island.is/auth-nest-tools' +import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' +import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' +import { SyslumennClientConfig } from '@island.is/clients/syslumenn' +import { AuditModule } from '@island.is/nest/audit' import { ConfigModule, IdsClientConfig, XRoadConfig, } from '@island.is/nest/config' -import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' -import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' -import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { FeatureFlagConfig } from '@island.is/nest/feature-flags' +import { environment } from '../environments' +import { AccessLogsModule } from './modules/accessLogs/accessLogs.module' +import { PersonalRepresentativesModule } from './modules/personalRepresentatives/personalRepresentatives.module' +import { PersonalRepresentativeTypesModule } from './modules/personalRepresentativeTypes/personalRepresentativeTypes.module' +import { RightTypesModule } from './modules/rightTypes/rightTypes.module' + @Module({ imports: [ AuditModule.forRoot(environment.audit), @@ -38,6 +41,7 @@ import { FeatureFlagConfig } from '@island.is/nest/feature-flags' CompanyRegistryConfig, XRoadConfig, FeatureFlagConfig, + SyslumennClientConfig, ], }), RightTypesModule, diff --git a/apps/services/auth/public-api/infra/auth-public-api.ts b/apps/services/auth/public-api/infra/auth-public-api.ts index 68c82c383cf0..28e81a88467a 100644 --- a/apps/services/auth/public-api/infra/auth-public-api.ts +++ b/apps/services/auth/public-api/infra/auth-public-api.ts @@ -1,5 +1,4 @@ -import { service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl' -import { json } from '../../../../../infra/src/dsl/dsl' +import { json, service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl' import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad' const REDIS_NODE_CONFIG = { @@ -64,12 +63,20 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-public-api'> => { // Origin for Android prod app 'android:apk-key-hash:EsLTUu5kaY7XPmMl2f7nbq4amu-PNzdYu3FecNf90wU', ]), + SYSLUMENN_HOST: { + dev: 'https://api.syslumenn.is/staging', + staging: 'https://api.syslumenn.is/staging', + prod: 'https://api.syslumenn.is', + }, + SYSLUMENN_TIMEOUT: '3000', }) .secrets({ IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET', NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME', + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD', }) .xroad(Base, Client, RskProcuring) .ingress({ diff --git a/apps/services/auth/public-api/src/app/app.module.ts b/apps/services/auth/public-api/src/app/app.module.ts index 299a9219a419..a6863c79a866 100644 --- a/apps/services/auth/public-api/src/app/app.module.ts +++ b/apps/services/auth/public-api/src/app/app.module.ts @@ -2,11 +2,15 @@ import { Module } from '@nestjs/common' import { SequelizeModule } from '@nestjs/sequelize' import { - SequelizeConfigService, DelegationConfig, PasskeysCoreConfig, + SequelizeConfigService, } from '@island.is/auth-api-lib' import { AuthModule } from '@island.is/auth-nest-tools' +import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' +import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' +import { SyslumennClientConfig } from '@island.is/clients/syslumenn' import { AuditModule } from '@island.is/nest/audit' import { ConfigModule, @@ -15,9 +19,6 @@ import { } from '@island.is/nest/config' import { FeatureFlagConfig } from '@island.is/nest/feature-flags' import { ProblemModule } from '@island.is/nest/problem' -import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' -import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' -import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' import { environment } from '../environments' import { DelegationsModule } from './modules/delegations/delegations.module' @@ -44,6 +45,7 @@ import { PasskeysModule } from './modules/passkeys/passkeys.module' CompanyRegistryConfig, XRoadConfig, PasskeysCoreConfig, + SyslumennClientConfig, ], }), ], diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index 6795fd24a227..d654243b8f1c 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -225,6 +225,8 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -291,6 +293,8 @@ services-auth-admin-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -308,6 +312,8 @@ services-auth-delegation-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' USER_NOTIFICATION_API_URL: 'http://web-user-notification.user-notification.svc.cluster.local' XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV' @@ -374,6 +380,8 @@ services-auth-delegation-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -404,6 +412,8 @@ services-auth-ids-api: PUBLIC_URL: 'https://identity-server.dev01.devland.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]' USER_PROFILE_CLIENT_URL: 'http://web-service-portal-api.service-portal.svc.cluster.local' XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is' @@ -498,6 +508,8 @@ services-auth-ids-api: NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD' NOVA_URL: '/k8s/services-auth/NOVA_URL' NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -574,6 +586,8 @@ services-auth-personal-representative: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -642,6 +656,8 @@ services-auth-personal-representative: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -735,6 +751,8 @@ services-auth-public-api: PUBLIC_URL: 'https://identity-server.dev01.devland.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -805,6 +823,8 @@ services-auth-public-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 96bafb5febeb..1e365eced0e1 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -222,6 +222,8 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' + SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -288,6 +290,8 @@ services-auth-admin-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -305,6 +309,8 @@ services-auth-delegation-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' + SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_TIMEOUT: '3000' USER_NOTIFICATION_API_URL: 'https://user-notification.internal.island.is' XROAD_BASE_PATH: 'http://securityserver.island.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS' @@ -371,6 +377,8 @@ services-auth-delegation-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -401,6 +409,8 @@ services-auth-ids-api: PUBLIC_URL: 'https://innskra.island.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' + SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_TIMEOUT: '3000' USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]' USER_PROFILE_CLIENT_URL: 'https://service-portal-api.internal.island.is' XROAD_BASE_PATH: 'http://securityserver.island.is' @@ -495,6 +505,8 @@ services-auth-ids-api: NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD' NOVA_URL: '/k8s/services-auth/NOVA_URL' NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -571,6 +583,8 @@ services-auth-personal-representative: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' + SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -631,6 +645,8 @@ services-auth-personal-representative: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -724,6 +740,8 @@ services-auth-public-api: PUBLIC_URL: 'https://innskra.island.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' + SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -794,6 +812,8 @@ services-auth-public-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index 7c5eeccdd189..3c2233c431e0 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -225,6 +225,8 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -291,6 +293,8 @@ services-auth-admin-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -308,6 +312,8 @@ services-auth-delegation-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' USER_NOTIFICATION_API_URL: 'http://web-user-notification.user-notification.svc.cluster.local' XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST' @@ -374,6 +380,8 @@ services-auth-delegation-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -404,6 +412,8 @@ services-auth-ids-api: PUBLIC_URL: 'https://identity-server.staging01.devland.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]' USER_PROFILE_CLIENT_URL: 'http://web-service-portal-api.service-portal.svc.cluster.local' XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is' @@ -498,6 +508,8 @@ services-auth-ids-api: NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD' NOVA_URL: '/k8s/services-auth/NOVA_URL' NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -574,6 +586,8 @@ services-auth-personal-representative: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -634,6 +648,8 @@ services-auth-personal-representative: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false @@ -727,6 +743,8 @@ services-auth-public-api: PUBLIC_URL: 'https://identity-server.staging01.devland.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: '' + SYSLUMENN_HOST: 'https://api.syslumenn.is/staging' + SYSLUMENN_TIMEOUT: '3000' 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' @@ -797,6 +815,8 @@ services-auth-public-api: DB_PASS: '/k8s/servicesauth/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' + SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' + SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts index bb6f5a332112..94e89cc29204 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts @@ -6,6 +6,8 @@ import startOfDay from 'date-fns/startOfDay' import { Op, Transaction } from 'sequelize' import { uuid } from 'uuidv4' +import { SyslumennService } from '@island.is/clients/syslumenn' +import { logger } from '@island.is/logging' import { AuthDelegationProvider, AuthDelegationType, @@ -18,14 +20,14 @@ import { ApiScope } from '../resources/models/api-scope.model' import { IdentityResource } from '../resources/models/identity-resource.model' import { DelegationProviderService } from './delegation-provider.service' import { DelegationConfig } from './DelegationConfig' +import { DelegationsIndexService } from './delegations-index.service' import { UpdateDelegationScopeDTO } from './dto/delegation-scope.dto' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' import { DelegationScope } from './models/delegation-scope.model' import { DelegationTypeModel } from './models/delegation-type.model' import { Delegation } from './models/delegation.model' import type { User } from '@island.is/auth-nest-tools' -import { DelegationDelegationType } from './models/delegation-delegation-type.model' - @Injectable() export class DelegationScopeService { constructor( @@ -40,6 +42,8 @@ export class DelegationScopeService { @Inject(DelegationConfig.KEY) private delegationConfig: ConfigType, private delegationProviderService: DelegationProviderService, + private readonly syslumennService: SyslumennService, + private readonly delegationsIndexService: DelegationsIndexService, ) {} async createOrUpdate( @@ -304,6 +308,55 @@ export class DelegationScopeService { return apiScopes.map((s) => s.name) } + private async findDistrictCommissionersRegistryScopesTo( + toNationalId: string, + fromNationalId: string, + ): Promise { + // if no valid delegation exists, return empty array + try { + const delegationFound = + await this.syslumennService.checkIfDelegationExists( + toNationalId, + fromNationalId, + ) + + if (!delegationFound) { + this.delegationsIndexService.removeDelegationRecord({ + fromNationalId, + toNationalId, + type: AuthDelegationType.LegalRepresentative, + provider: AuthDelegationProvider.DistrictCommissionersRegistry, + }) + return [] + } + } catch (error) { + logger.error( + `Failed checking if delegation exists at provider '${AuthDelegationProvider.DistrictCommissionersRegistry}'`, + ) + return [] + } + + // else return all enabled scopes for this provider and provided delegation types + const apiScopes = await this.apiScopeModel.findAll({ + attributes: ['name'], + where: { + enabled: true, + }, + include: [ + { + model: DelegationTypeModel, + required: true, + where: { + id: AuthDelegationType.LegalRepresentative, + provider: AuthDelegationProvider.DistrictCommissionersRegistry, + }, + }, + ], + }) + + return apiScopes.map((s) => s.name) + } + private async findAllAutomaticScopes(): Promise { const apiScopes = await this.apiScopeModel.findAll({ attributes: ['name'], @@ -372,6 +425,16 @@ export class DelegationScopeService { ) } + if ( + providers.includes(AuthDelegationProvider.DistrictCommissionersRegistry) + ) + scopePromises.push( + this.findDistrictCommissionersRegistryScopesTo( + user.nationalId, + fromNationalId, + ), + ) + const scopeSets = await Promise.all(scopePromises) let scopes = ([] as string[]).concat(...scopeSets) diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts index 30eb53b694c3..56355428a63e 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common' +import { BadRequestException, Injectable } from '@nestjs/common' import { InjectModel } from '@nestjs/sequelize' import { User } from '@island.is/auth-nest-tools' @@ -6,7 +6,8 @@ import { IndividualDto, NationalRegistryClientService, } from '@island.is/clients/national-registry-v2' -import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' +import { SyslumennService } from '@island.is/clients/syslumenn' +import { logger } from '@island.is/logging' import { FeatureFlagService, Features } from '@island.is/nest/feature-flags' import { AuthDelegationProvider, @@ -53,8 +54,6 @@ interface FindAvailableInput { @Injectable() export class DelegationsIncomingService { constructor( - @Inject(LOGGER_PROVIDER) - protected readonly logger: Logger, @InjectModel(Client) private clientModel: typeof Client, @InjectModel(ClientAllowedScope) @@ -69,6 +68,7 @@ export class DelegationsIncomingService { private delegationProviderService: DelegationProviderService, private nationalRegistryClient: NationalRegistryClientService, private readonly featureFlagService: FeatureFlagService, + private readonly syslumennService: SyslumennService, ) {} async findAllValid( @@ -272,6 +272,45 @@ export class DelegationsIncomingService { return [...mergedDelegationMap.values()] } + async verifyDelegationAtProvider( + user: User, + fromNationalId: string, + delegationTypes: AuthDelegationType[], + ): Promise { + const providers = await this.delegationProviderService.findProviders( + delegationTypes, + ) + + if ( + providers.includes(AuthDelegationProvider.DistrictCommissionersRegistry) + ) { + try { + const delegationFound = + await this.syslumennService.checkIfDelegationExists( + user.nationalId, + fromNationalId, + ) + + if (delegationFound) { + return true + } else { + this.delegationsIndexService.removeDelegationRecord({ + fromNationalId, + toNationalId: user.nationalId, + type: AuthDelegationType.LegalRepresentative, + provider: AuthDelegationProvider.DistrictCommissionersRegistry, + }) + } + } catch (error) { + logger.error( + `Failed checking if delegation exists at provider '${AuthDelegationProvider.DistrictCommissionersRegistry}'`, + ) + } + } + + return false + } + private async getAvailableDistrictCommissionersRegistryDelegations( user: User, types: AuthDelegationType[], diff --git a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts index c74aaca8a940..e5f79d1aab6c 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts @@ -1,40 +1,41 @@ import { Module } from '@nestjs/common' import { SequelizeModule } from '@nestjs/sequelize' -import { NationalRegistryClientModule } from '@island.is/clients/national-registry-v2' import { RskRelationshipsClientModule } from '@island.is/clients-rsk-relationships' +import { NationalRegistryClientModule } from '@island.is/clients/national-registry-v2' import { CompanyRegistryClientModule } from '@island.is/clients/rsk/company-registry' +import { SyslumennClientModule } from '@island.is/clients/syslumenn' import { FeatureFlagModule } from '@island.is/nest/feature-flags' -import { UserSystemNotificationModule } from '../user-notification' import { ClientAllowedScope } from '../clients/models/client-allowed-scope.model' import { Client } from '../clients/models/client.model' import { PersonalRepresentativeModule } from '../personal-representative/personal-representative.module' +import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model' +import { ApiScopeUserAccess } from '../resources/models/api-scope-user-access.model' import { ApiScope } from '../resources/models/api-scope.model' import { IdentityResource } from '../resources/models/identity-resource.model' import { ResourcesModule } from '../resources/resources.module' +import { UserIdentitiesModule } from '../user-identities/user-identities.module' +import { UserSystemNotificationModule } from '../user-notification' +import { DelegationAdminCustomService } from './admin/delegation-admin-custom.service' +import { DelegationProviderService } from './delegation-provider.service' import { DelegationScopeService } from './delegation-scope.service' -import { DelegationsOutgoingService } from './delegations-outgoing.service' -import { DelegationsService } from './delegations.service' -import { DelegationsIncomingService } from './delegations-incoming.service' -import { DelegationScope } from './models/delegation-scope.model' -import { Delegation } from './models/delegation.model' -import { NamesService } from './names.service' -import { DelegationsIncomingWardService } from './delegations-incoming-ward.service' import { IncomingDelegationsCompanyService } from './delegations-incoming-company.service' import { DelegationsIncomingCustomService } from './delegations-incoming-custom.service' import { DelegationsIncomingRepresentativeService } from './delegations-incoming-representative.service' -import { ApiScopeUserAccess } from '../resources/models/api-scope-user-access.model' -import { DelegationIndex } from './models/delegation-index.model' -import { DelegationIndexMeta } from './models/delegation-index-meta.model' +import { DelegationsIncomingWardService } from './delegations-incoming-ward.service' +import { DelegationsIncomingService } from './delegations-incoming.service' import { DelegationsIndexService } from './delegations-index.service' -import { UserIdentitiesModule } from '../user-identities/user-identities.module' -import { DelegationTypeModel } from './models/delegation-type.model' -import { DelegationProviderModel } from './models/delegation-provider.model' -import { DelegationProviderService } from './delegation-provider.service' -import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model' -import { DelegationAdminCustomService } from './admin/delegation-admin-custom.service' +import { DelegationsOutgoingService } from './delegations-outgoing.service' +import { DelegationsService } from './delegations.service' import { DelegationDelegationType } from './models/delegation-delegation-type.model' +import { DelegationIndexMeta } from './models/delegation-index-meta.model' +import { DelegationIndex } from './models/delegation-index.model' +import { DelegationProviderModel } from './models/delegation-provider.model' +import { DelegationScope } from './models/delegation-scope.model' +import { DelegationTypeModel } from './models/delegation-type.model' +import { Delegation } from './models/delegation.model' +import { NamesService } from './names.service' @Module({ imports: [ @@ -61,6 +62,7 @@ import { DelegationDelegationType } from './models/delegation-delegation-type.mo DelegationDelegationType, ]), UserSystemNotificationModule, + SyslumennClientModule, ], providers: [ DelegationsService, diff --git a/libs/clients/syslumenn/src/clientConfig.json b/libs/clients/syslumenn/src/clientConfig.json index 22ef380cfc45..3c6ac47e291a 100644 --- a/libs/clients/syslumenn/src/clientConfig.json +++ b/libs/clients/syslumenn/src/clientConfig.json @@ -472,6 +472,58 @@ ] } }, + "/api/Logradamadur/{audkenni}": { + "get": { + "tags": ["Syslumenn"], + "operationId": "Logradamadur_Get", + "parameters": [ + { + "type": "string", + "name": "audkenni", + "in": "path", + "required": true, + "x-nullable": false + }, + { + "type": "string", + "name": "kennitala", + "in": "query", + "x-nullable": true + } + ], + "responses": { + "200": { + "x-nullable": false, + "description": "Success", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/LogradamadurSvar" + } + } + }, + "500": { + "x-nullable": false, + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/ProblemDetails" + } + }, + "401": { + "x-nullable": false, + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ProblemDetails" + } + } + }, + "security": [ + { + "JWT Token": [] + } + ] + } + }, "/api/Logmannalisti": { "get": { "tags": ["Syslumenn"], @@ -862,6 +914,99 @@ ] } }, + "/v1/Heimagistingar/{audkenni}": { + "put": { + "tags": ["Syslumenn"], + "operationId": "Heimagistingar_Put", + "consumes": ["application/json", "text/json", "application/*+json"], + "parameters": [ + { + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UppfaeraHeimagistingarModel" + }, + "x-nullable": false + }, + { + "type": "string", + "name": "audkenni", + "in": "path", + "required": true, + "x-nullable": false + } + ], + "responses": { + "200": { + "description": "" + }, + "401": { + "x-nullable": false, + "description": "", + "schema": { + "$ref": "#/definitions/ProblemDetails" + } + }, + "default": { + "x-nullable": false, + "description": "", + "schema": { + "$ref": "#/definitions/ProblemDetails" + } + } + }, + "security": [ + { + "JWT Token": [] + } + ] + }, + "get": { + "tags": ["Syslumenn"], + "operationId": "Heimagistingar_Get", + "parameters": [ + { + "type": "string", + "name": "audkenni", + "in": "path", + "required": true, + "x-nullable": false + } + ], + "responses": { + "200": { + "x-nullable": false, + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/HeimagistingarModel" + } + } + }, + "401": { + "x-nullable": false, + "description": "", + "schema": { + "$ref": "#/definitions/ProblemDetails" + } + }, + "default": { + "x-nullable": false, + "description": "", + "schema": { + "$ref": "#/definitions/ProblemDetails" + } + } + }, + "security": [ + { + "JWT Token": [] + } + ] + } + }, "/v1/VirkarHeimagistingar/{audkenni}": { "get": { "tags": ["Syslumenn"], @@ -1154,13 +1299,7 @@ }, { "type": "string", - "name": "Hjonaefni1", - "in": "query", - "x-nullable": true - }, - { - "type": "string", - "name": "Hjonaefni2", + "name": "kennitala", "in": "query", "x-nullable": true } @@ -1356,7 +1495,7 @@ "type": "string", "name": "malsnumer", "in": "query", - "x-nullable": false + "x-nullable": true }, { "type": "string", @@ -3356,6 +3495,18 @@ } } }, + "LogradamadurSvar": { + "type": "object", + "properties": { + "kennitala": { + "type": "string" + }, + "gildirTil": { + "type": "string", + "format": "date-time" + } + } + }, "Logmenn": { "type": "object", "properties": { @@ -3672,6 +3823,62 @@ } } }, + "UppfaeraHeimagistingarModel": { + "type": "object", + "properties": { + "leyfi": { + "type": "array", + "items": { + "$ref": "#/definitions/UppfaeraHeimagistingDetail" + } + } + } + }, + "UppfaeraHeimagistingDetail": { + "type": "object", + "properties": { + "tegund": { + "type": "string" + }, + "numer": { + "type": "string" + }, + "markadsefniSlod": { + "type": "string" + }, + "fannst": { + "type": "boolean" + } + } + }, + "HeimagistingarModel": { + "type": "object", + "properties": { + "leyfi": { + "type": "array", + "items": { + "$ref": "#/definitions/HeimagistingDetail" + } + } + } + }, + "HeimagistingDetail": { + "type": "object", + "properties": { + "tegund": { + "type": "string" + }, + "numerLeyfist": { + "type": "string" + }, + "virkt": { + "type": "boolean" + }, + "utgefid": { + "type": "string" + } + } + }, "VirkarHeimagistingar": { "type": "object", "properties": { @@ -4012,6 +4219,12 @@ }, "skyring": { "type": "string" + }, + "simi": { + "type": "string" + }, + "netfang": { + "type": "string" } } }, diff --git a/libs/clients/syslumenn/src/lib/syslumennClient.service.ts b/libs/clients/syslumenn/src/lib/syslumennClient.service.ts index 92dcdcb6990c..d606178c5ff7 100644 --- a/libs/clients/syslumenn/src/lib/syslumennClient.service.ts +++ b/libs/clients/syslumenn/src/lib/syslumennClient.service.ts @@ -1,80 +1,83 @@ +import { Inject, Injectable } from '@nestjs/common' +import startOfDay from 'date-fns/startOfDay' + +import { AuthHeaderMiddleware } from '@island.is/auth-nest-tools' +import { createEnhancedFetch } from '@island.is/clients/middlewares' + +import { + Configuration, + InnsigludSkjol, + LogradamadurSvar, + Skilabod, + SvarSkeyti, + SyslumennApi, + VedbandayfirlitRegluverkGeneralSvar, + VedbondTegundAndlags, + VirkLeyfiGetRequest, +} from '../../gen/fetch' +import { SyslumennClientConfig } from './syslumennClient.config' import { AlcoholLicence, - SyslumennAuction, - Homestay, - PaginatedOperatingLicenses, - OperatingLicensesCSV, + AssetName, + AssetType, + Attachment, + Broker, CertificateInfoResponse, - DistrictCommissionerAgencies, DataUploadResponse, - Person, - Attachment, - AssetType, - MortgageCertificate, - MortgageCertificateValidation, - AssetName, + DistrictCommissionerAgencies, + EstateInfo, EstateRegistrant, EstateRelations, - EstateInfo, - RealEstateAgent, + Homestay, + InheritanceReportInfo, + InheritanceTax, Lawyer, - Broker, + ManyPropertyDetail, + MortgageCertificate, + MortgageCertificateValidation, + OperatingLicensesCSV, + PaginatedOperatingLicenses, + Person, PropertyDetail, + RealEstateAgent, + RegistryPerson, + SyslumennAuction, TemporaryEventLicence, VehicleRegistration, - RegistryPerson, - InheritanceTax, - InheritanceReportInfo, - ManyPropertyDetail, } from './syslumennClient.types' import { - mapSyslumennAuction, - mapHomestay, - mapPaginatedOperatingLicenses, - mapOperatingLicensesCSV, - mapCertificateInfo, - mapDistrictCommissionersAgenciesResponse, - mapDataUploadResponse, + cleanPropertyNumber, constructUploadDataObject, + mapAlcoholLicence, mapAssetName, - mapEstateRegistrant, - mapEstateInfo, - mapRealEstateAgent, - mapLawyer, mapBroker, - mapAlcoholLicence, - cleanPropertyNumber, - mapTemporaryEventLicence, - mapMasterLicence, - mapVehicle, + mapCertificateInfo, + mapDataUploadResponse, mapDepartedToRegistryPerson, - mapInheritanceTax, + mapDistrictCommissionersAgenciesResponse, + mapEstateInfo, + mapEstateRegistrant, mapEstateToInheritanceReportInfo, + mapHomestay, + mapInheritanceTax, mapJourneymanLicence, + mapLawyer, + mapMasterLicence, + mapOperatingLicensesCSV, + mapPaginatedOperatingLicenses, mapProfessionRight, - mapVehicleResponse, + mapPropertyCertificate, + mapRealEstateAgent, mapRealEstateResponse, mapShipResponse, - mapPropertyCertificate, + mapSyslumennAuction, + mapTemporaryEventLicence, + mapVehicle, + mapVehicleResponse, } from './syslumennClient.utils' -import { Injectable, Inject } from '@nestjs/common' -import { - SyslumennApi, - SvarSkeyti, - Configuration, - VirkLeyfiGetRequest, - VedbondTegundAndlags, - Skilabod, - VedbandayfirlitRegluverkGeneralSvar, - InnsigludSkjol, -} from '../../gen/fetch' -import { SyslumennClientConfig } from './syslumennClient.config' -import type { ConfigType } from '@island.is/nest/config' -import { AuthHeaderMiddleware } from '@island.is/auth-nest-tools' -import { createEnhancedFetch } from '@island.is/clients/middlewares' +import type { ConfigType } from '@island.is/nest/config' const UPLOAD_DATA_SUCCESS = 'Gögn móttekin' - @Injectable() export class SyslumennService { constructor( @@ -674,4 +677,22 @@ export class SyslumennService { kennitala: nationalId, }) } + + async checkIfDelegationExists( + toNationalId: string, + fromNationalId: string, + ): Promise { + const { id, api } = await this.createApi() + const delegations: LogradamadurSvar[] = await api.logradamadurGet({ + audkenni: id, + kennitala: toNationalId, + }) + + return delegations.some( + (delegation) => + delegation.kennitala === fromNationalId && + (!delegation.gildirTil || + delegation.gildirTil > startOfDay(new Date())), + ) + } } From 67f34d42ea6506940223fe8e05b64d552b995ff7 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:48:39 +0000 Subject: [PATCH 116/173] feat(web): Add default header for HVE organization (#16126) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Organization/Wrapper/OrganizationWrapper.css.ts | 13 +++++++++++++ .../Organization/Wrapper/OrganizationWrapper.tsx | 11 ++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts index 5247afc5c39c..0c31cf2d7f01 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts @@ -104,3 +104,16 @@ export const rikissaksoknariHeaderGridContainerWidth = style([ export const rikissaksoknariHeaderGridContainerSubpage = rikissaksoknariHeaderGridContainerBase + +export const hveHeaderGridContainer = style({ + display: 'grid', + maxWidth: '1342px', + margin: '0 auto', + borderBottom: '8px solid #F01E28', + ...themeUtils.responsiveStyle({ + lg: { + gridTemplateRows: '315px', + gridTemplateColumns: '65fr 35fr', + }, + }), +}) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index bd017feeb446..c5e02e1841aa 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -436,7 +436,16 @@ export const OrganizationHeader: React.FC< /> ) case 'hve': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Tue, 24 Sep 2024 12:32:31 +0000 Subject: [PATCH 117/173] fix(ids-auth-api): add missing semicolon from seeder script (#16134) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../20240917153226-set-also-for-delegated-user-false.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js b/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js index 03879f2dbbee..63d0ef1513bb 100644 --- a/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js +++ b/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js @@ -4,7 +4,7 @@ BEGIN; UPDATE api_scope SET also_for_delegated_user = false - WHERE name = '@island.is/signature-collection' + WHERE name = '@island.is/signature-collection'; COMMIT; `) @@ -15,7 +15,7 @@ BEGIN; UPDATE api_scope SET also_for_delegated_user = true - WHERE name = '@island.is/signature-collection' + WHERE name = '@island.is/signature-collection'; COMMIT; `) From d1c83d42a7b5fe5d25bfd6ab6056e8a284b68e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 24 Sep 2024 15:03:26 +0200 Subject: [PATCH 118/173] fix(j-s): Signed URL Lifetime (#16128) * Uses a fresh AWS S# session when getting signed URLs for the court of appeals robot * Updates unit tests --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/app/modules/aws-s3/awsS3.service.ts | 19 +++++++++++++++---- .../src/app/modules/file/file.service.ts | 10 +++++++++- .../getCaseFileSignedUrl.spec.ts | 1 + .../deliverCaseFileToCourtOfAppeals.spec.ts | 1 + .../getCaseFileSignedUrl.spec.ts | 1 + 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts b/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts index 04c57cf5f55e..3ae180e14d27 100644 --- a/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts +++ b/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts @@ -125,13 +125,18 @@ export class AwsS3Service { caseType: CaseType, key?: string, timeToLive?: number, + useFreshSession = false, ): Promise { if (!key) { throw new Error('Key is required') } return new Promise((resolve, reject) => { - this.s3.getSignedUrl( + const s3 = useFreshSession + ? new S3({ region: this.config.region }) + : this.s3 + + s3.getSignedUrl( 'getObject', { Bucket: this.config.bucket, @@ -155,6 +160,7 @@ export class AwsS3Service { force: boolean, confirmContent: (content: Buffer) => Promise, timeToLive?: number, + useFreshSession = false, ): Promise { if (!key) { throw new Error('Key is required') @@ -167,7 +173,12 @@ export class AwsS3Service { const confirmedKey = formatConfirmedIndictmentCaseKey(key) if (!force && (await this.objectExists(caseType, confirmedKey))) { - return this.getSignedUrl(caseType, confirmedKey, timeToLive) + return this.getSignedUrl( + caseType, + confirmedKey, + timeToLive, + useFreshSession, + ) } const confirmedContent = await this.getObject(caseType, key).then( @@ -175,11 +186,11 @@ export class AwsS3Service { ) if (!confirmedContent) { - return this.getSignedUrl(caseType, key, timeToLive) + return this.getSignedUrl(caseType, key, timeToLive, useFreshSession) } return this.putObject(caseType, confirmedKey, confirmedContent).then(() => - this.getSignedUrl(caseType, confirmedKey, timeToLive), + this.getSignedUrl(caseType, confirmedKey, timeToLive, useFreshSession), ) } diff --git a/apps/judicial-system/backend/src/app/modules/file/file.service.ts b/apps/judicial-system/backend/src/app/modules/file/file.service.ts index 3bda7c821d48..3aa552b87baf 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.service.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.service.ts @@ -405,6 +405,7 @@ export class FileService { theCase: Case, file: CaseFile, timeToLive?: number, + useFreshSession = false, ): Promise { if (this.shouldGetConfirmedDocument(file, theCase)) { return this.awsS3Service.getConfirmedIndictmentCaseSignedUrl( @@ -414,10 +415,16 @@ export class FileService { (content: Buffer) => this.confirmIndictmentCaseFile(theCase, file, content), timeToLive, + useFreshSession, ) } - return this.awsS3Service.getSignedUrl(theCase.type, file.key, timeToLive) + return this.awsS3Service.getSignedUrl( + theCase.type, + file.key, + timeToLive, + useFreshSession, + ) } async getCaseFileSignedUrl( @@ -559,6 +566,7 @@ export class FileService { theCase, file, this.config.robotS3TimeToLiveGet, + true, ) return this.courtService diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts index 7865f80f3787..c61ce8d1cbf9 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts @@ -82,6 +82,7 @@ describe('FileController - Get case file signed url', () => { theCase.type, key, undefined, + false, ) expect(then.result).toEqual({ url }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts index d6d768626368..bf3546195e32 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts @@ -106,6 +106,7 @@ describe('InternalFileController - Deliver case file to court of appeals', () => theCase.type, key, mockFileConfig.robotS3TimeToLiveGet, + true, ) expect(mockCourtService.updateAppealCaseWithFile).toHaveBeenCalledWith( user, diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts index 0f847a92485b..a25ae133189e 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts @@ -96,6 +96,7 @@ describe('LimitedAccessFileController - Get case file signed url', () => { theCase.type, key, undefined, + false, ) }) }) From 942b852a22d60ed900f11192688bfca92c8e5c8b Mon Sep 17 00:00:00 2001 From: juni-haukur <158475136+juni-haukur@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:45:55 +0000 Subject: [PATCH 119/173] fix(service-portal): Tweaks for parliamentary signature collection (#16123) * Tweaks for parliamentary signature collection * add presidential check --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: albinagu <47886428+albinagu@users.noreply.github.com> --- .../{canSign.input.ts => canSignFromPaper.input.ts} | 5 ++++- .../src/lib/signatureCollection.resolver.ts | 10 ++++------ .../src/lib/signatureCollection.service.ts | 10 ++++++++++ .../src/lib/signatureCollectionAdmin.resolver.ts | 4 ++-- .../src/lib/signature-collection.service.ts | 5 +++-- .../signature-collection/src/hooks/graphql/queries.ts | 4 ++-- .../signature-collection/src/hooks/index.ts | 9 +++++++-- .../OwnerView/ViewList/Signees/PaperSignees.tsx | 1 + 8 files changed, 33 insertions(+), 15 deletions(-) rename libs/api/domains/signature-collection/src/lib/dto/{canSign.input.ts => canSignFromPaper.input.ts} (62%) diff --git a/libs/api/domains/signature-collection/src/lib/dto/canSign.input.ts b/libs/api/domains/signature-collection/src/lib/dto/canSignFromPaper.input.ts similarity index 62% rename from libs/api/domains/signature-collection/src/lib/dto/canSign.input.ts rename to libs/api/domains/signature-collection/src/lib/dto/canSignFromPaper.input.ts index 3f00b419735f..1aa493259781 100644 --- a/libs/api/domains/signature-collection/src/lib/dto/canSign.input.ts +++ b/libs/api/domains/signature-collection/src/lib/dto/canSignFromPaper.input.ts @@ -2,8 +2,11 @@ import { IsString } from 'class-validator' import { Field, InputType } from '@nestjs/graphql' @InputType() -export class SignatureCollectionCanSignInput { +export class SignatureCollectionCanSignFromPaperInput { @Field() @IsString() signeeNationalId!: string + @Field() + @IsString() + listId!: string } diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts index a1f783de22de..a6edb5d5df50 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts @@ -30,7 +30,7 @@ import { CurrentSignee } from './decorators/signee.decorator' import { ApiScope } from '@island.is/auth/scopes' import { SignatureCollectionCancelListsInput } from './dto/cencelLists.input' import { SignatureCollectionIdInput } from './dto/collectionId.input' -import { SignatureCollectionCanSignInput } from './dto/canSign.input' +import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input' import { SignatureCollectionAddListsInput } from './dto/addLists.input' import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input' import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input' @@ -133,13 +133,11 @@ export class SignatureCollectionResolver { @Query(() => Boolean) @AccessRequirement(OwnerAccess.AllowActor) @Audit() - async signatureCollectionCanSign( - @Args('input') input: SignatureCollectionCanSignInput, + async signatureCollectionCanSignFromPaper( + @Args('input') input: SignatureCollectionCanSignFromPaperInput, @CurrentUser() user: User, ): Promise { - return ( - await this.signatureCollectionService.signee(user, input.signeeNationalId) - ).canSign + return await this.signatureCollectionService.canSignFromPaper(user, input) } @Scopes(ApiScope.signatureCollection) diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts index c7cd95b72e88..455958cf3a62 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts @@ -18,6 +18,7 @@ import { SignatureCollectionAddListsInput } from './dto/addLists.input' import { SignatureCollectionOwnerInput } from './dto/owner.input' import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input' import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input' +import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input' @Injectable() export class SignatureCollectionService { @@ -142,4 +143,13 @@ export class SignatureCollectionService { input, ) } + + async canSignFromPaper( + user: User, + input: SignatureCollectionCanSignFromPaperInput, + ): Promise { + const signee = await this.signee(user, input.signeeNationalId) + const list = await this.list(input.listId, user) + return signee.canSign && list.area.id === signee.area?.id + } } diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts index 58f264dd8b90..29986111d194 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts @@ -31,7 +31,7 @@ import { SignatureCollectionNationalIdInput } from './dto/nationalId.input' import { SignatureCollectionSignatureIdInput } from './dto/signatureId.input' import { SignatureCollectionIdInput } from './dto/collectionId.input' import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input' -import { SignatureCollectionCanSignInput } from './dto/canSign.input' +import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input' import { ReasonKey } from '@island.is/clients/signature-collection' import { CanSignInfo } from './models/canSignInfo.model' @@ -53,7 +53,7 @@ export class SignatureCollectionAdminResolver { async signatureCollectionAdminCanSignInfo( @CurrentUser() user: User, - @Args('input') input: SignatureCollectionCanSignInput, + @Args('input') input: SignatureCollectionCanSignFromPaperInput, ): Promise { const canSignInfo = await this.signatureCollectionService.getCanSignInfo( user, diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.ts index d9df57925283..c2e1df76630a 100644 --- a/libs/clients/signature-collection/src/lib/signature-collection.service.ts +++ b/libs/clients/signature-collection/src/lib/signature-collection.service.ts @@ -358,7 +358,7 @@ export class SignatureCollectionClientService { async getSignedList(auth: User): Promise { const { signatures } = await this.getSignee(auth) - const { endTime } = await this.currentCollection() + const { endTime, isPresidential } = await this.currentCollection() if (!signatures) { return null } @@ -372,13 +372,14 @@ export class SignatureCollectionClientService { ) const isExtended = list.endTime > endTime const signedThisPeriod = signature.isInitialType === !isExtended + const canUnsignDigital = isPresidential ? signature.isDigital : true return { signedDate: signature.created, isDigital: signature.isDigital, pageNumber: signature.pageNumber, isValid: signature.valid, canUnsign: - signature.isDigital && + canUnsignDigital && signature.valid && list.active && signedThisPeriod, 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 45d50fdbe81c..4eae87f09acd 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts @@ -159,7 +159,7 @@ export const GetCurrentCollection = gql` ` export const GetCanSign = gql` - query Query($input: SignatureCollectionCanSignInput!) { - signatureCollectionCanSign(input: $input) + query Query($input: SignatureCollectionCanSignFromPaperInput!) { + signatureCollectionCanSignFromPaper(input: $input) } ` diff --git a/libs/service-portal/signature-collection/src/hooks/index.ts b/libs/service-portal/signature-collection/src/hooks/index.ts index 9c3800136b8e..1c26cfd03b61 100644 --- a/libs/service-portal/signature-collection/src/hooks/index.ts +++ b/libs/service-portal/signature-collection/src/hooks/index.ts @@ -149,18 +149,23 @@ export const useGetCurrentCollection = () => { } } -export const useGetCanSign = (signeeId: string, isValidId: boolean) => { +export const useGetCanSign = ( + signeeId: string, + listId: string, + isValidId: boolean, +) => { const { data: getCanSignData, loading: loadingCanSign } = useQuery( GetCanSign, { variables: { input: { signeeNationalId: signeeId, + listId: listId, }, }, skip: !signeeId || signeeId.length !== 10 || !isValidId, }, ) - const canSign = getCanSignData?.signatureCollectionCanSign ?? false + const canSign = getCanSignData?.signatureCollectionCanSignFromPaper ?? false return { canSign, loadingCanSign } } diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx index 877cf5b5bd8f..5519f717939d 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx @@ -44,6 +44,7 @@ export const PaperSignees = ({ }) const { canSign, loadingCanSign } = useGetCanSign( nationalIdInput, + listId, nationalId.isValid(nationalIdInput), ) From 1f684cfe256477768603f36b8da86fd59ce67e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 24 Sep 2024 16:01:18 +0200 Subject: [PATCH 120/173] fix(j-s): E2E Tests (#16133) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../HearingArrangements/HearingArrangements.tsx | 3 +-- .../regression/custody-tests.spec.ts | 2 +- .../regression/indictment-tests.spec.ts | 15 ++++++++++----- .../regression/search-warrant-tests.spec.ts | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/HearingArrangements/HearingArrangements.tsx b/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/HearingArrangements/HearingArrangements.tsx index d574cb3d8868..14f54d752318 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/HearingArrangements/HearingArrangements.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/RestrictionCase/HearingArrangements/HearingArrangements.tsx @@ -93,8 +93,7 @@ export const HearingArrangements = () => { [formatMessage, router, setWorkingCase, transitionCase, workingCase], ) - const stepIsValid = - isHearingArrangementsStepValidRC(workingCase) || isTransitioningCase + const stepIsValid = isHearingArrangementsStepValidRC(workingCase) return ( { .fill(randomPoliceCaseNumber()) await page.getByRole('button', { name: 'Skrá númer' }).click() await page.getByRole('checkbox').first().check() - await page.locator('input[name=accusedName]').fill(faker.name.findName()) + await page.locator('input[name=inputName]').fill(faker.name.findName()) await page.locator('input[name=accusedAddress]').fill('Einhversstaðar 1') await page.locator('#defendantGender').click() await page.locator('#react-select-defendantGender-option-0').click() diff --git a/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts b/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts index 032baf8fb428..b8b723faa91c 100644 --- a/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts +++ b/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts @@ -39,11 +39,11 @@ test.describe.serial('Indictment tests', () => { await page .getByRole('checkbox', { name: 'Ákærði er ekki með íslenska kennitölu' }) .check() - await page.getByTestId('nationalId').click() - await page.getByTestId('nationalId').fill('01.01.2000') - await page.getByTestId('accusedName').click() - await page.getByTestId('accusedName').fill(accusedName) - await page.getByTestId('accusedName').press('Tab') + await page.getByTestId('inputNationalId').click() + await page.getByTestId('inputNationalId').fill('01.01.2000') + await page.getByTestId('inputName').click() + await page.getByTestId('inputName').fill(accusedName) + await page.getByTestId('inputName').press('Tab') await page.getByTestId('accusedAddress').fill('Testgata 12') await page.locator('#defendantGender').click() await page.locator('#react-select-defendantGender-option-0').click() @@ -78,10 +78,15 @@ test.describe.serial('Indictment tests', () => { page.getByText('Játar sök').click(), verifyRequestCompletion(page, '/api/graphql', 'UpdateDefendant'), ]) + await Promise.all([ + page.getByText('Nei').last().click(), + verifyRequestCompletion(page, '/api/graphql', 'UpdateCase'), + ]) await Promise.all([ page.getByTestId('continueButton').click(), verifyRequestCompletion(page, '/api/graphql', 'Case'), ]) + // Case files await expect(page).toHaveURL(`/akaera/domskjol/${caseId}`) diff --git a/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts b/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts index 40fd809eac8f..64e139713d95 100644 --- a/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts +++ b/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts @@ -41,7 +41,7 @@ test.describe.serial('Search warrant tests', () => { await page.locator('#type').click() await page.locator('#react-select-type-option-0').click() await page.getByRole('checkbox').first().check() - await page.locator('input[name=accusedName]').fill(faker.name.findName()) + await page.locator('input[name=inputName]').fill(faker.name.findName()) await page.locator('input[name=accusedAddress]').fill('Einhversstaðar 1') await page.locator('#defendantGender').click() await page.locator('#react-select-defendantGender-option-0').click() From e9c1d7c90d15ee1e61216cc1400a3f5ad3d50e0a Mon Sep 17 00:00:00 2001 From: juni-haukur <158475136+juni-haukur@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:54:24 +0000 Subject: [PATCH 121/173] fix(signature-collection): Not stop paper signatures when already signed (#16137) --- .../src/lib/signatureCollection.service.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts index 455958cf3a62..a3ef87c27516 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts @@ -148,8 +148,17 @@ export class SignatureCollectionService { user: User, input: SignatureCollectionCanSignFromPaperInput, ): Promise { - const signee = await this.signee(user, input.signeeNationalId) + const signee = await this.signatureCollectionClientService.getSignee( + user, + input.signeeNationalId, + ) const list = await this.list(input.listId, user) - return signee.canSign && list.area.id === signee.area?.id + // Current signatures should not prevent paper signatures + const canSign = + signee.canSign || + (signee.canSignInfo?.length === 1 && + signee.canSignInfo[0] === ReasonKey.AlreadySigned) + + return canSign && list.area.id === signee.area?.id } } From f55aa9203f06f15ea05c7bfdfbed7918c862ace1 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:11:08 +0000 Subject: [PATCH 122/173] feat(web): Add default header for rikislogmadur organization (#16135) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Wrapper/OrganizationWrapper.css.ts | 29 +++++++++++++++++++ .../Wrapper/OrganizationWrapper.tsx | 11 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts index 0c31cf2d7f01..5ccd9797f6b2 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts @@ -105,6 +105,35 @@ export const rikissaksoknariHeaderGridContainerWidth = style([ export const rikissaksoknariHeaderGridContainerSubpage = rikissaksoknariHeaderGridContainerBase +export const rikislogmadurHeaderGridContainerWidthBase = style({ + display: 'grid', + maxWidth: '1342px', + margin: '0 auto', + backgroundBlendMode: 'saturation', + backgroundRepeat: 'no-repeat', + background: + 'linear-gradient(178.67deg, rgba(0, 61, 133, 0.2) 1.87%, rgba(0, 61, 133, 0.3) 99.6%)', + ...themeUtils.responsiveStyle({ + lg: { + gridTemplateRows: '315px', + gridTemplateColumns: '60fr 40fr', + }, + }), +}) + +export const rikislogmadurHeaderGridContainerWidth = style([ + rikislogmadurHeaderGridContainerWidthBase, + themeUtils.responsiveStyle({ + lg: { + background: `linear-gradient(178.67deg, rgba(0, 61, 133, 0.2) 1.87%, rgba(0, 61, 133, 0.3) 99.6%), + url('https://images.ctfassets.net/8k0h54kbe6bj/40IgMzNknBQUINDZZwblR/6c7dfdcf0acb3612f2bf61d912c3dd46/rikislogmadur-header-image.png') no-repeat right`, + }, + }), +]) + +export const rikislogmadurHeaderGridContainerWidthSubpage = + rikislogmadurHeaderGridContainerWidthBase + export const hveHeaderGridContainer = style({ display: 'grid', maxWidth: '1342px', diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index c5e02e1841aa..52b6baa452ed 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -377,7 +377,16 @@ export const OrganizationHeader: React.FC< /> ) case 'rikislogmadur': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Tue, 24 Sep 2024 15:33:52 +0000 Subject: [PATCH 123/173] feat(portals-admin): Set up for Parliamentary Lists (#15892) * feat(portals-admin): Set up for Parliamentary Lists * chore: nx format:write update dirty files * initial page / component setup * linting fix * updates * bera saman description * cleanup * restructure * loaders restructure * addressing coderabbit * tweak * cr * folder rename * lists title * cr * chore: nx format:write update dirty files * tweaks --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../signature-collection/src/lib/messages.ts | 54 ++++++++- .../src/lib/navigation.ts | 13 ++- .../signature-collection/src/lib/paths.ts | 10 +- .../AllLists => loaders}/AllLists.loader.ts | 14 +-- .../{screens/List => loaders}/List.loader.ts | 22 ++-- .../getAllSignatureLists.graphql | 0 .../getCollectionStatus.graphql | 0 .../listGraphql}/getListSignees.graphql | 0 .../listGraphql}/getListStatus.graphql | 0 .../listGraphql}/getSignatureList.graphql | 0 .../admin/signature-collection/src/module.tsx | 41 ++++++- .../Constituency/index.tsx | 108 ++++++++++++++++++ .../src/screens-parliamentary/List/index.tsx | 50 ++++++++ .../src/screens-parliamentary/index.tsx | 89 +++++++++++++++ .../finishCollectionProcess.graphql | 0 .../completeCollectionProcessing/index.tsx | 0 .../components/reviewCandidates/index.tsx | 0 .../reviewCandidates/removeCandidate.graphql | 0 .../AllLists/index.tsx | 18 +-- .../List/components/listInfoAlert/index.tsx | 0 .../List/components/paperUpload/index.tsx | 0 .../paperUpload/paperUpload.graphql | 0 .../List/components/skeleton.tsx | 0 .../List/index.tsx | 6 +- .../compareLists/compareLists.graphql | 0 .../compareLists/index.tsx | 16 +-- .../removeSignatureFromList.graphql | 0 .../compareLists/skeleton.tsx | 0 .../completeReview/index.tsx | 12 +- .../completeReview/toggleListReview.graphql | 0 .../createCollection/candidateLookup.graphql | 0 .../createCollection/createCollection.graphql | 0 .../createCollection/index.tsx | 5 +- .../createCollection/utils.ts | 2 +- .../emptyState/EmptyImgSmall.tsx | 0 .../emptyState/index.tsx | 0 .../extendDeadline/extendDeadline.graphql | 0 .../extendDeadline/index.tsx | 4 +- .../signees/index.tsx} | 29 ++--- .../sortSignees/index.tsx | 2 +- 40 files changed, 412 insertions(+), 83 deletions(-) rename libs/portals/admin/signature-collection/src/{screens/AllLists => loaders}/AllLists.loader.ts (89%) rename libs/portals/admin/signature-collection/src/{screens/List => loaders}/List.loader.ts (85%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/graphql => loaders/allListsGraphql}/getAllSignatureLists.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/graphql => loaders/allListsGraphql}/getCollectionStatus.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/List/graphql => loaders/listGraphql}/getListSignees.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/List/graphql => loaders/listGraphql}/getListStatus.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/List/graphql => loaders/listGraphql}/getSignatureList.graphql (100%) create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/AllLists/components/completeCollectionProcessing/index.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/AllLists/components/reviewCandidates/index.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/AllLists/components/reviewCandidates/removeCandidate.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/AllLists/index.tsx (95%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/List/components/listInfoAlert/index.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/List/components/paperUpload/index.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/List/components/paperUpload/paperUpload.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/List/components/skeleton.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens => screens-presidential}/List/index.tsx (95%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/compareLists/compareLists.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/compareLists/index.tsx (93%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/compareLists/removeSignatureFromList.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/compareLists/skeleton.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens/List/components => shared-components}/completeReview/index.tsx (92%) rename libs/portals/admin/signature-collection/src/{screens/List/components => shared-components}/completeReview/toggleListReview.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/createCollection/candidateLookup.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/createCollection/createCollection.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/createCollection/index.tsx (98%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/createCollection/utils.ts (93%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/emptyState/EmptyImgSmall.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens/AllLists/components => shared-components}/emptyState/index.tsx (100%) rename libs/portals/admin/signature-collection/src/{screens/List/components => shared-components}/extendDeadline/extendDeadline.graphql (100%) rename libs/portals/admin/signature-collection/src/{screens/List/components => shared-components}/extendDeadline/index.tsx (97%) rename libs/portals/admin/signature-collection/src/{screens/List/components/signees.tsx => shared-components/signees/index.tsx} (88%) rename libs/portals/admin/signature-collection/src/{screens/List/components => shared-components}/sortSignees/index.tsx (96%) diff --git a/libs/portals/admin/signature-collection/src/lib/messages.ts b/libs/portals/admin/signature-collection/src/lib/messages.ts index 5f2efaaa5285..a74985c729d8 100644 --- a/libs/portals/admin/signature-collection/src/lib/messages.ts +++ b/libs/portals/admin/signature-collection/src/lib/messages.ts @@ -9,7 +9,12 @@ export const m = defineMessages({ }, signatureListsTitle: { id: 'admin-portal.signature-collection:signatureLists', - defaultMessage: 'Meðmælasafnanir', + defaultMessage: 'Forsetakosningar', + description: '', + }, + signatureListsConstituencyTitle: { + id: 'admin-portal.signature-collection:signatureListsConstituencyTitle', + defaultMessage: 'Kjördæmi', description: '', }, signatureListsDescription: { @@ -150,6 +155,11 @@ export const m = defineMessages({ defaultMessage: 'Skoða söfnun', description: '', }, + viewConstituency: { + id: 'admin-portal.signature-collection:viewConstituency', + defaultMessage: 'Skoða kjördæmi', + description: '', + }, noLists: { id: 'admin-portal.signature-collection:noLists', defaultMessage: 'Engin söfnun í gangi', @@ -417,7 +427,7 @@ export const m = defineMessages({ compareListsDescription: { id: 'admin-portal.signature-collection:compareListsDescription', defaultMessage: - 'Fulltrúar í yfirkjörstjórnum og frambjóðendur geta ekki mælt með framboði.', + 'Fulltrúar í yfirkjörstjórnum og frambjóðendur geta ekki mælt með framboði', description: '', }, compareListsModalDescription: { @@ -482,6 +492,46 @@ export const m = defineMessages({ }, }) +export const parliamentaryMessages = defineMessages({ + listTitle: { + id: 'admin-portal.signature-collection-parliamentary:listTitle', + defaultMessage: 'Alþingiskosningar', + description: '', + }, + signatureListsTitle: { + id: 'admin-portal.signature-collection-parliamentary:signatureLists', + defaultMessage: 'Alþingiskosningar', + description: '', + }, + signatureListsDescription: { + id: 'admin-portal.signature-collection-parliamentary:signatureListsDescription', + defaultMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + description: '', + }, + signatureListsIntro: { + id: 'admin-portal.signature-collection-parliamentary:signatureListsIntro', + defaultMessage: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed etiam, ut inquit, in vita et in voluptate locum ponamus, isdem et in dolore et in odio.', + description: '', + }, + compareListsButton: { + id: 'admin-portal.signature-collection-parliamentary:compareListsButton', + defaultMessage: 'Bera saman', + description: '', + }, + compareListsDescription: { + id: 'admin-portal.signature-collection-parliamentary:compareListsDescription', + defaultMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + description: '', + }, + singleConstituencyIntro: { + id: 'admin-portal.signature-collection-parliamentary:singleConstituencyIntro', + defaultMessage: + 'Hér er yfirlit yfir allar meðmælasafnanir sem stofnaðar hafa verið í', + description: '', + }, +}) + export const createCollectionErrorMessages = defineMessages({ age: { id: 'admin-portal.signature-collection:error.age', diff --git a/libs/portals/admin/signature-collection/src/lib/navigation.ts b/libs/portals/admin/signature-collection/src/lib/navigation.ts index 3fd05cb1b1c7..62626e9198c8 100644 --- a/libs/portals/admin/signature-collection/src/lib/navigation.ts +++ b/libs/portals/admin/signature-collection/src/lib/navigation.ts @@ -1,18 +1,23 @@ import { PortalNavigationItem } from '@island.is/portals/core' import { SignatureCollectionPaths } from './paths' -import { m } from './messages' +import { m, parliamentaryMessages } from './messages' export const signatureCollectionNavigation: PortalNavigationItem = { name: m.signatureListsTitle, icon: { - icon: 'settings', + icon: 'receipt', }, description: m.signatureListsDescription, - path: SignatureCollectionPaths.SignatureLists, + path: SignatureCollectionPaths.ParliamentaryRoot, children: [ + { + name: parliamentaryMessages.listTitle, + path: SignatureCollectionPaths.ParliamentaryRoot, + activeIfExact: true, + }, { name: m.collectionTitle, - path: SignatureCollectionPaths.SignatureLists, + path: SignatureCollectionPaths.PresidentialLists, activeIfExact: true, }, ], diff --git a/libs/portals/admin/signature-collection/src/lib/paths.ts b/libs/portals/admin/signature-collection/src/lib/paths.ts index de55e159b3e2..4627cffeaa4c 100644 --- a/libs/portals/admin/signature-collection/src/lib/paths.ts +++ b/libs/portals/admin/signature-collection/src/lib/paths.ts @@ -1,4 +1,10 @@ export enum SignatureCollectionPaths { - SignatureLists = '/medmaelasofnun', - SignatureList = '/medmaelasofnun/:id', + // Presidential + PresidentialLists = '/medmaelasofnun', + PresidentialList = '/medmaelasofnun/:listId', + + // Parliamentary + ParliamentaryRoot = '/althingiskosningar', + ParliamentaryConstituency = '/althingiskosningar/:constituencyName', + ParliamentaryConstituencyList = '/althingiskosningar/:constituencyName/:listId', } diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/AllLists.loader.ts b/libs/portals/admin/signature-collection/src/loaders/AllLists.loader.ts similarity index 89% rename from libs/portals/admin/signature-collection/src/screens/AllLists/AllLists.loader.ts rename to libs/portals/admin/signature-collection/src/loaders/AllLists.loader.ts index ce93146aa674..9b123c9a44f2 100644 --- a/libs/portals/admin/signature-collection/src/screens/AllLists/AllLists.loader.ts +++ b/libs/portals/admin/signature-collection/src/loaders/AllLists.loader.ts @@ -1,8 +1,4 @@ import type { WrappedLoaderFn } from '@island.is/portals/core' -import { - AllListsDocument, - AllListsQuery, -} from './graphql/getAllSignatureLists.generated' import { SignatureCollection, SignatureCollectionList, @@ -10,7 +6,11 @@ import { import { CollectionDocument, CollectionQuery, -} from './graphql/getCollectionStatus.generated' +} from './allListsGraphql/getCollectionStatus.generated' +import { + AllListsDocument, + AllListsQuery, +} from './allListsGraphql/getAllSignatureLists.generated' export interface ListsLoaderReturn { allLists: SignatureCollectionList[] @@ -19,9 +19,7 @@ export interface ListsLoaderReturn { } export const listsLoader: WrappedLoaderFn = ({ client }) => { - return async ({ - params, - }): Promise<{ + return async (): Promise<{ allLists: SignatureCollectionList[] collectionStatus: string collection: SignatureCollection diff --git a/libs/portals/admin/signature-collection/src/screens/List/List.loader.ts b/libs/portals/admin/signature-collection/src/loaders/List.loader.ts similarity index 85% rename from libs/portals/admin/signature-collection/src/screens/List/List.loader.ts rename to libs/portals/admin/signature-collection/src/loaders/List.loader.ts index 174eb769cc41..67ca3e477350 100644 --- a/libs/portals/admin/signature-collection/src/screens/List/List.loader.ts +++ b/libs/portals/admin/signature-collection/src/loaders/List.loader.ts @@ -1,20 +1,20 @@ import type { WrappedLoaderFn } from '@island.is/portals/core' -import { - ListbyidDocument, - ListbyidQuery, -} from './graphql/getSignatureList.generated' import { SignatureCollectionList, SignatureCollectionSignature, } from '@island.is/api/schema' import { - ListStatusDocument, - ListStatusQuery, -} from './graphql/getListStatus.generated' + ListbyidDocument, + ListbyidQuery, +} from './listGraphql/getSignatureList.generated' import { SignaturesDocument, SignaturesQuery, -} from './graphql/getListSignees.generated' +} from './listGraphql/getListSignees.generated' +import { + ListStatusDocument, + ListStatusQuery, +} from './listGraphql/getListStatus.generated' export const listLoader: WrappedLoaderFn = ({ client }) => { return async ({ @@ -29,7 +29,7 @@ export const listLoader: WrappedLoaderFn = ({ client }) => { fetchPolicy: 'network-only', variables: { input: { - listId: params.id, + listId: params.listId, }, }, }) @@ -39,7 +39,7 @@ export const listLoader: WrappedLoaderFn = ({ client }) => { fetchPolicy: 'network-only', variables: { input: { - listId: params.id, + listId: params.listId, }, }, }) @@ -49,7 +49,7 @@ export const listLoader: WrappedLoaderFn = ({ client }) => { fetchPolicy: 'network-only', variables: { input: { - listId: params.id, + listId: params.listId, }, }, }) diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getAllSignatureLists.graphql b/libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getAllSignatureLists.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getAllSignatureLists.graphql rename to libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getAllSignatureLists.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getCollectionStatus.graphql b/libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getCollectionStatus.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getCollectionStatus.graphql rename to libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getCollectionStatus.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/List/graphql/getListSignees.graphql b/libs/portals/admin/signature-collection/src/loaders/listGraphql/getListSignees.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/graphql/getListSignees.graphql rename to libs/portals/admin/signature-collection/src/loaders/listGraphql/getListSignees.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/List/graphql/getListStatus.graphql b/libs/portals/admin/signature-collection/src/loaders/listGraphql/getListStatus.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/graphql/getListStatus.graphql rename to libs/portals/admin/signature-collection/src/loaders/listGraphql/getListStatus.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/List/graphql/getSignatureList.graphql b/libs/portals/admin/signature-collection/src/loaders/listGraphql/getSignatureList.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/graphql/getSignatureList.graphql rename to libs/portals/admin/signature-collection/src/loaders/listGraphql/getSignatureList.graphql diff --git a/libs/portals/admin/signature-collection/src/module.tsx b/libs/portals/admin/signature-collection/src/module.tsx index 7bd7a7a410b1..3bdd8fd55999 100644 --- a/libs/portals/admin/signature-collection/src/module.tsx +++ b/libs/portals/admin/signature-collection/src/module.tsx @@ -2,12 +2,20 @@ import { PortalModule } from '@island.is/portals/core' import { lazy } from 'react' import { m } from './lib/messages' import { SignatureCollectionPaths } from './lib/paths' -import { listsLoader } from './screens/AllLists/AllLists.loader' import { AdminPortalScope } from '@island.is/auth/scopes' -import { listLoader } from './screens/List/List.loader' +import { listsLoader } from './loaders/AllLists.loader' +import { listLoader } from './loaders/List.loader' -const AllLists = lazy(() => import('./screens/AllLists')) -const List = lazy(() => import('./screens/List')) +/* parliamentary */ +const ParliamentaryRoot = lazy(() => import('./screens-parliamentary')) +const ParliamentaryConstituency = lazy(() => + import('./screens-parliamentary/Constituency'), +) +const ParliamentaryList = lazy(() => import('./screens-parliamentary/List')) + +/* presidential */ +const AllLists = lazy(() => import('./screens-presidential/AllLists')) +const List = lazy(() => import('./screens-presidential/List')) const allowedScopes: string[] = [ AdminPortalScope.signatureCollectionManage, @@ -20,9 +28,30 @@ export const signatureCollectionModule: PortalModule = { enabled: ({ userInfo }) => userInfo.scopes.some((scope) => allowedScopes.includes(scope)), routes: (props) => [ + /* ------ Parliamentary ------ */ + { + name: m.signatureListsTitle, + path: SignatureCollectionPaths.ParliamentaryRoot, + element: , + loader: listsLoader(props), + }, + { + name: m.signatureListsConstituencyTitle, + path: SignatureCollectionPaths.ParliamentaryConstituency, + element: , + loader: listsLoader(props), + }, + { + name: m.singleList, + path: SignatureCollectionPaths.ParliamentaryConstituencyList, + element: , + loader: listLoader(props), + }, + + /* ------ Presidential ------ */ { name: m.signatureListsTitle, - path: SignatureCollectionPaths.SignatureLists, + path: SignatureCollectionPaths.PresidentialLists, element: ( { + const { formatMessage } = useLocale() + const navigate = useNavigate() + + const { collection, allLists } = useLoaderData() as ListsLoaderReturn + const { constituencyName } = useParams() as { constituencyName: string } + + const constituencyLists = allLists.filter( + (list) => list.area.name === constituencyName, + ) + + return ( + + + + + + + + + + + + {formatMessage(m.totalListResults) + + ': ' + + constituencyLists.length} + + {constituencyLists?.length > 0 && ( + + )} + + + {constituencyLists.map((list) => ( + { + navigate( + SignatureCollectionPaths.ParliamentaryConstituencyList.replace( + ':constituencyName', + constituencyName, + ).replace(':listId', list.id), + ) + }, + }} + /> + ))} + + + + + + + ) +} + +export default Constituency diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx new file mode 100644 index 000000000000..9f0cca263f80 --- /dev/null +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx @@ -0,0 +1,50 @@ +import { GridColumn, GridContainer, GridRow } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { IntroHeader, PortalNavigation } from '@island.is/portals/core' +import { signatureCollectionNavigation } from '../../lib/navigation' +import { m, parliamentaryMessages } from '../../lib/messages' +import { useLoaderData } from 'react-router-dom' +import { SignatureCollectionList } from '@island.is/api/schema' +import ActionExtendDeadline from '../../shared-components/extendDeadline' +import Signees from '../../shared-components/signees' +import ActionReviewComplete from '../../shared-components/completeReview' + +const List = () => { + const { formatMessage } = useLocale() + const { list } = useLoaderData() as { + list: SignatureCollectionList + } + + return ( + + + + + + + + + + + + + + ) +} + +export default List diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx new file mode 100644 index 000000000000..54b6428860c7 --- /dev/null +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx @@ -0,0 +1,89 @@ +import { + ActionCard, + FilterInput, + GridColumn, + GridContainer, + GridRow, + Stack, + Box, + Text, +} from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { IntroHeader, PortalNavigation } from '@island.is/portals/core' +import { signatureCollectionNavigation } from '../lib/navigation' +import { m, parliamentaryMessages } from '../lib/messages' +import { useLoaderData, useNavigate } from 'react-router-dom' +import { SignatureCollectionPaths } from '../lib/paths' +import CompareLists from '../shared-components/compareLists' +import { ListsLoaderReturn } from '../loaders/AllLists.loader' + +const ParliamentaryRoot = () => { + const { formatMessage } = useLocale() + + const navigate = useNavigate() + const { collection } = useLoaderData() as ListsLoaderReturn + + return ( + + + + + + + + + console.log('search')} + placeholder={formatMessage(m.searchInListPlaceholder)} + backgroundColor="blue" + /> + + + {formatMessage(m.totalListResults) + ' ' + collection?.areas.length} + + + {collection?.areas.map((area) => ( + { + navigate( + SignatureCollectionPaths.ParliamentaryConstituency.replace( + ':constituencyName', + area.name, + ), + ) + }, + }} + /> + ))} + + + + + + ) +} + +export default ParliamentaryRoot diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/index.tsx similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/index.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/index.tsx diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/index.tsx similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/index.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/index.tsx diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/removeCandidate.graphql b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/removeCandidate.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/removeCandidate.graphql rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/removeCandidate.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx similarity index 95% rename from libs/portals/admin/signature-collection/src/screens/AllLists/index.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx index 624dadff74a1..b10fd2ce50a5 100644 --- a/libs/portals/admin/signature-collection/src/screens/AllLists/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx @@ -26,16 +26,16 @@ import { countryAreas, pageSize, } from '../../lib/utils' -import CompareLists from './components/compareLists' import { format as formatNationalId } from 'kennitala' -import CreateCollection from './components/createCollection' import electionsCommitteeLogo from '../../../assets/electionsCommittee.svg' import nationalRegistryLogo from '../../../assets/nationalRegistry.svg' import ActionCompleteCollectionProcessing from './components/completeCollectionProcessing' import ListInfo from '../List/components/listInfoAlert' -import { ListsLoaderReturn } from './AllLists.loader' -import EmptyState from './components/emptyState' +import EmptyState from '../../shared-components/emptyState' import ReviewCandidates from './components/reviewCandidates' +import CompareLists from '../../shared-components/compareLists' +import { ListsLoaderReturn } from '../../loaders/AllLists.loader' +import CreateCollection from '../../shared-components/createCollection' const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => { const { formatMessage } = useLocale() @@ -226,7 +226,7 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => { - {lists?.length > 0 ? ( + {lists?.length > 0 && collection.isPresidential ? ( <> {filters.input.length > 0 || @@ -282,8 +282,8 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => { icon: 'arrowForward', onClick: () => { navigate( - SignatureCollectionPaths.SignatureList.replace( - ':id', + SignatureCollectionPaths.PresidentialList.replace( + ':listId', list.id, ), ) @@ -311,7 +311,7 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => { /> )} - {lists?.length > 0 && ( + {lists?.length > 0 && collection.isPresidential && ( { )} - {lists?.length > 0 && ( + {lists?.length > 0 && collection.isPresidential && ( )} diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/listInfoAlert/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/listInfoAlert/index.tsx similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/components/listInfoAlert/index.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/listInfoAlert/index.tsx diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/index.tsx similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/index.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/index.tsx diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/paperUpload.graphql b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/paperUpload.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/paperUpload.graphql rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/paperUpload.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/skeleton.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/skeleton.tsx similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/List/components/skeleton.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/skeleton.tsx diff --git a/libs/portals/admin/signature-collection/src/screens/List/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/index.tsx similarity index 95% rename from libs/portals/admin/signature-collection/src/screens/List/index.tsx rename to libs/portals/admin/signature-collection/src/screens-presidential/List/index.tsx index 4610b450ad2d..89d3aca6dc65 100644 --- a/libs/portals/admin/signature-collection/src/screens/List/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-presidential/List/index.tsx @@ -11,15 +11,15 @@ import { GridRow, Text, } from '@island.is/island-ui/core' -import Signees from './components/signees' -import ActionExtendDeadline from './components/extendDeadline' -import ActionReviewComplete from './components/completeReview' import PaperUpload from './components/paperUpload' import ListInfo from './components/listInfoAlert' import electionsCommitteeLogo from '../../../assets/electionsCommittee.svg' import nationalRegistryLogo from '../../../assets/nationalRegistry.svg' import { format as formatNationalId } from 'kennitala' import { ListStatus } from '../../lib/utils' +import ActionReviewComplete from '../../shared-components/completeReview' +import Signees from '../../shared-components/signees' +import ActionExtendDeadline from '../../shared-components/extendDeadline' export const List = ({ allowedToProcess }: { allowedToProcess: boolean }) => { const { list, listStatus } = useLoaderData() as { diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/compareLists.graphql b/libs/portals/admin/signature-collection/src/shared-components/compareLists/compareLists.graphql similarity index 100% rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/compareLists.graphql rename to libs/portals/admin/signature-collection/src/shared-components/compareLists/compareLists.graphql diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/index.tsx b/libs/portals/admin/signature-collection/src/shared-components/compareLists/index.tsx similarity index 93% rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/index.tsx rename to libs/portals/admin/signature-collection/src/shared-components/compareLists/index.tsx index 9ede275c65a1..4dd05e5091d1 100644 --- a/libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/index.tsx +++ b/libs/portals/admin/signature-collection/src/shared-components/compareLists/index.tsx @@ -8,15 +8,15 @@ import { toast, } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' -import { m } from '../../../../lib/messages' import { useState } from 'react' import { Modal } from '@island.is/react/components' import { useBulkCompareMutation } from './compareLists.generated' import { format as formatNationalId } from 'kennitala' import { SignatureCollectionSignature } from '@island.is/api/schema' -import { createFileList, getFileData } from '../../../../lib/utils' import { Skeleton } from './skeleton' import { useUnsignAdminMutation } from './removeSignatureFromList.generated' +import { m } from '../../lib/messages' +import { createFileList, getFileData } from '../../lib/utils' const CompareLists = ({ collectionId }: { collectionId: string }) => { const { formatMessage } = useLocale() @@ -57,7 +57,7 @@ const CompareLists = ({ collectionId }: { collectionId: string }) => { }, }) - if (res.data && res.data.signatureCollectionAdminUnsign.success) { + if (res.data?.signatureCollectionAdminUnsign.success) { toast.success(formatMessage(m.unsignFromListSuccess)) setUploadResults( uploadResults?.filter((result: SignatureCollectionSignature) => { @@ -84,20 +84,20 @@ const CompareLists = ({ collectionId }: { collectionId: string }) => { return ( - + {formatMessage(m.compareListsDescription)} + + )} + + { + handleUpdateCivilClaimant({ + caseId: workingCase.id, + civilClaimantId: civilClaimant.id, + nationalId: null, + noNationalId: !civilClaimant.noNationalId, + }) + }} + backgroundColor="white" + large + filled + /> + + + { + if (val.length < 11) { + setNationalIdNotFound(false) + } else if (val.length === 11) { + handleCivilClaimantNationalIdBlur( + val, + civilClaimant.noNationalId, + civilClaimant.id, + ) + } + + updateCivilClaimantState( + { + caseId: workingCase.id, + civilClaimantId: civilClaimant.id ?? '', + nationalId: val, + }, + setWorkingCase, + ) + }} + onBlur={(val) => + handleCivilClaimantNationalIdBlur( + val, + civilClaimant.noNationalId, + civilClaimant.id, + ) + } + /> + {civilClaimant.nationalId?.length === 11 && + nationalIdNotFound && ( + + {formatMessage( + core.nationalIdNotFoundInNationalRegistry, + )} + + )} + + + updateCivilClaimantState( + { + caseId: workingCase.id, + civilClaimantId: civilClaimant.id ?? '', + name: val, + }, + setWorkingCase, + ) + } + onBlur={(val) => + handleCivilClaimantNameBlur(val, civilClaimant.id) + } + required + /> + + + + {civilClaimant.hasSpokesperson && ( + <> + + + + handleUpdateCivilClaimant({ + caseId: workingCase.id, + civilClaimantId: civilClaimant.id, + spokespersonIsLawyer: true, + }) + } + checked={Boolean( + civilClaimant.spokespersonIsLawyer, + )} + /> + + + + handleUpdateCivilClaimant({ + caseId: workingCase.id, + civilClaimantId: civilClaimant.id, + spokespersonIsLawyer: false, + }) + } + checked={ + civilClaimant.spokespersonIsLawyer === false + } + /> + + + + + + { + handleUpdateCivilClaimant({ + caseId: workingCase.id, + civilClaimantId: civilClaimant.id, + caseFilesSharedWithSpokesperson: + !civilClaimant.caseFilesSharedWithSpokesperson, + }) + }} + disabled={ + civilClaimant.spokespersonIsLawyer === null || + civilClaimant.spokespersonIsLawyer === undefined + } + tooltip={formatMessage( + strings.civilClaimantShareFilesWithDefenderTooltip, + )} + backgroundColor="white" + large + filled + /> + + )} + + + ))} + + + + + )} { + const { formatMessage } = useIntl() + + const [createCivilClaimantMutation, { loading: isCreatingCivilClaimant }] = + useCreateCivilClaimantMutation() + const [deleteCivilClaimantMutation] = useDeleteCivilClaimantMutation() + const [updateCivilClaimantMutation] = useUpdateCivilClaimantMutation() + + const createCivilClaimant = useCallback( + async (civilClaimant: CreateCivilClaimantInput) => { + try { + if (!isCreatingCivilClaimant) { + const { data } = await createCivilClaimantMutation({ + variables: { + input: civilClaimant, + }, + }) + + if (data) { + return data.createCivilClaimant?.id + } + } + return null + } catch (error) { + toast.error(formatMessage(errors.createCivilClaimant)) + return null + } + }, + [createCivilClaimantMutation, formatMessage, isCreatingCivilClaimant], + ) + + const deleteCivilClaimant = useCallback( + async (caseId: string, civilClaimantId: string) => { + try { + const { data } = await deleteCivilClaimantMutation({ + variables: { input: { caseId, civilClaimantId } }, + }) + + return Boolean(data?.deleteCivilClaimant.deleted) + } catch (error) { + toast.error(formatMessage(errors.deleteCivilClaimant)) + return false + } + }, + [deleteCivilClaimantMutation, formatMessage], + ) + + const updateCivilClaimant = useCallback( + async (updateCivilClaimant: UpdateCivilClaimantInput) => { + try { + const { data } = await updateCivilClaimantMutation({ + variables: { + input: updateCivilClaimant, + }, + }) + + return Boolean(data) + } catch (error) { + toast.error(formatMessage(errors.updateCivilClaimant)) + return false + } + }, + [formatMessage, updateCivilClaimantMutation], + ) + + const updateCivilClaimantState = useCallback( + ( + update: UpdateCivilClaimantInput, + setWorkingCase: Dispatch>, + ) => { + setWorkingCase((prevWorkingCase: Case) => { + if (!prevWorkingCase.civilClaimants) { + return prevWorkingCase + } + const indexOfCivilClaimantToUpdate = + prevWorkingCase.civilClaimants.findIndex( + (civilClaimant) => civilClaimant.id === update.civilClaimantId, + ) + + if (indexOfCivilClaimantToUpdate === -1) { + return prevWorkingCase + } else { + const newCivilClaimants = [...prevWorkingCase.civilClaimants] + + newCivilClaimants[indexOfCivilClaimantToUpdate] = { + ...newCivilClaimants[indexOfCivilClaimantToUpdate], + ...update, + } as CivilClaimant + + return { ...prevWorkingCase, civilClaimants: newCivilClaimants } + } + }) + }, + [], + ) + + const setAndSendCivilClaimantToServer = useCallback( + ( + update: UpdateCivilClaimantInput, + setWorkingCase: Dispatch>, + ) => { + updateCivilClaimantState(update, setWorkingCase) + updateCivilClaimant(update) + }, + [updateCivilClaimant, updateCivilClaimantState], + ) + + return { + createCivilClaimant, + deleteCivilClaimant, + updateCivilClaimant, + updateCivilClaimantState, + setAndSendCivilClaimantToServer, + } +} + +export default useCivilClaimants diff --git a/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/updateCivilClaimant.graphql b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/updateCivilClaimant.graphql new file mode 100644 index 000000000000..6b589542eb24 --- /dev/null +++ b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/updateCivilClaimant.graphql @@ -0,0 +1,5 @@ +mutation UpdateCivilClaimant($input: UpdateCivilClaimantInput!) { + updateCivilClaimant(input: $input) { + id + } +} diff --git a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts index 7b14e6eecd68..66987e7b513b 100644 --- a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts @@ -50,13 +50,10 @@ const useDefendants = () => { variables: { input: { caseId, defendantId } }, }) - if (data?.deleteDefendant?.deleted) { - return true - } else { - return false - } + return Boolean(data?.deleteDefendant?.deleted) } catch (error) { - formatMessage(errors.deleteDefendant) + toast.error(formatMessage(errors.deleteDefendant)) + return false } }, [deleteDefendantMutation, formatMessage], @@ -71,13 +68,10 @@ const useDefendants = () => { }, }) - if (data) { - return true - } else { - return false - } + return Boolean(data) } catch (error) { toast.error(formatMessage(errors.updateDefendant)) + return false } }, [formatMessage, updateDefendantMutation], diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts index dae538e76314..b2162dc1c6da 100644 --- a/apps/judicial-system/web/src/utils/validate.ts +++ b/apps/judicial-system/web/src/utils/validate.ts @@ -271,10 +271,21 @@ export const isProcessingStepValidIndictments = ( workingCase.hasCivilClaims !== null && workingCase.hasCivilClaims !== undefined + const allCivilClaimantsAreValid = workingCase.hasCivilClaims + ? workingCase.civilClaimants?.every( + (civilClaimant) => + civilClaimant.name && + (civilClaimant.noNationalId || + (civilClaimant.nationalId && + civilClaimant.nationalId.replace('-', '').length === 10)), + ) + : true + return Boolean( workingCase.prosecutor && workingCase.court && hasCivilClaimSelected && + allCivilClaimantsAreValid && defendantsAreValid(), ) } From a85feae2d8365ee16e4263c5ffefb1326c92d9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3n=20Bjarni=20=C3=93lafsson?= <92530555+jonbjarnio@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:09:34 +0000 Subject: [PATCH 131/173] fix(ojoi): Update form context (#16147) * Using setvalues to update the form context so updateApplication mutation updates the application with correct values. * Fixed import --- .../src/components/signatures/AddCommitteeMember.tsx | 5 +++++ .../src/components/signatures/AddRegularMember.tsx | 5 +++++ .../src/components/signatures/AddRegularSignature.tsx | 9 ++++++++- .../src/components/signatures/Chairman.tsx | 5 +++++ .../src/components/signatures/CommitteeMember.tsx | 5 +++++ .../src/components/signatures/Institution.tsx | 7 +++++++ .../src/components/signatures/RegularMember.tsx | 5 +++++ .../src/components/signatures/RemoveComitteeMember.tsx | 5 +++++ .../src/components/signatures/RemoveRegularMember.tsx | 5 +++++ .../src/components/signatures/RemoveRegularSignature.tsx | 5 +++++ .../src/screens/RequirementsScreen.tsx | 2 +- 11 files changed, 56 insertions(+), 2 deletions(-) diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx index d094f6b8e98e..26c31423553b 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx @@ -9,6 +9,7 @@ import { } from '../../lib/constants' import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils' import set from 'lodash/set' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -20,6 +21,8 @@ export const AddCommitteeMember = ({ applicationId }: Props) => { applicationId, }) + const { setValue } = useFormContext() + const onAddCommitteeMember = () => { const { signature, currentAnswers } = getCommitteeAnswers( structuredClone(application.answers), @@ -37,6 +40,8 @@ export const AddCommitteeMember = ({ applicationId }: Props) => { withExtraMember, ) + setValue(InputFields.signature.committee, withExtraMember) + updateApplication(updatedAnswers) } } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx index 4d6aeb79bfc4..840c0aa11962 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx @@ -9,6 +9,7 @@ import { } from '../../lib/constants' import { getEmptyMember, getRegularAnswers } from '../../lib/utils' import set from 'lodash/set' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -21,6 +22,8 @@ export const AddRegularMember = ({ applicationId, signatureIndex }: Props) => { applicationId, }) + const { setValue } = useFormContext() + const onAddMember = () => { const { signature, currentAnswers } = getRegularAnswers( structuredClone(application.answers), @@ -47,6 +50,8 @@ export const AddRegularMember = ({ applicationId, signatureIndex }: Props) => { updatedRegularSignature, ) + setValue(InputFields.signature.regular, updatedRegularSignature) + updateApplication(updatedAnswers) } } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx index 1d3ce373b3ed..7eaa66895463 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx @@ -15,6 +15,7 @@ import { MAXIMUM_REGULAR_SIGNATURE_COUNT, ONE, } from '../../lib/constants' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -26,6 +27,8 @@ export const AddRegularSignature = ({ applicationId }: Props) => { applicationId, }) + const { setValue } = useFormContext() + const onAddInstitution = () => { const { signature, currentAnswers } = getRegularAnswers( structuredClone(application.answers), @@ -37,12 +40,16 @@ export const AddRegularSignature = ({ applicationId }: Props) => { DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT, )?.pop() + const updatedSignature = [...signature, newSignature] + const updatedAnswers = set( currentAnswers, InputFields.signature.regular, - [...signature, newSignature], + updatedSignature, ) + setValue(InputFields.signature.regular, updatedSignature) + updateApplication(updatedAnswers) } } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx index e95a9acbb706..3b0362ccb297 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx @@ -13,6 +13,7 @@ import { SignatureMember } from './Member' import set from 'lodash/set' import * as styles from './Signatures.css' import * as z from 'zod' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -27,6 +28,8 @@ export const Chairman = ({ applicationId, member }: Props) => { applicationId, }) + const { setValue } = useFormContext() + const handleChairmanChange = (value: string, key: keyof MemberProperties) => { const { signature, currentAnswers } = getCommitteeAnswers( application.answers, @@ -57,6 +60,8 @@ export const Chairman = ({ applicationId, member }: Props) => { updatedCommitteeSignature, ) + setValue(InputFields.signature.committee, updatedCommitteeSignature) + return updatedSignatures } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx index a269ded1ae8b..ae96183ff5bc 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx @@ -14,6 +14,7 @@ import { memberItemSchema } from '../../lib/dataSchema' import { SignatureMember } from './Member' import * as z from 'zod' import { RemoveCommitteeMember } from './RemoveComitteeMember' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -33,6 +34,8 @@ export const CommitteeMember = ({ applicationId, }) + const { setValue } = useFormContext() + const handleMemberChange = ( value: string, key: keyof MemberProperties, @@ -77,6 +80,8 @@ export const CommitteeMember = ({ updatedCommitteeSignature, ) + setValue(InputFields.signature.committee, updatedCommitteeSignature) + return updatedSignatures } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx index 5a70d5b1512e..1f73a2f8f921 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx @@ -27,6 +27,7 @@ import { import { z } from 'zod' import { signatureInstitutionSchema } from '../../lib/dataSchema' import { RemoveRegularSignature } from './RemoveRegularSignature' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string type: SignatureType @@ -49,6 +50,8 @@ export const InstitutionSignature = ({ applicationId, }) + const { setValue } = useFormContext() + const handleInstitutionChange = ( value: string, key: SignatureInstitutionKeys, @@ -88,6 +91,8 @@ export const InstitutionSignature = ({ updatedRegularSignature, ) + setValue(InputFields.signature[type], updatedRegularSignature) + return updatedSignatures } @@ -114,6 +119,8 @@ export const InstitutionSignature = ({ }, ) + setValue(InputFields.signature[type], updatedCommitteeSignature) + return updatedCommitteeSignature } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx index d1e48b57c55c..443bdb010e7d 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx @@ -15,6 +15,7 @@ import { memberItemSchema } from '../../lib/dataSchema' import { SignatureMember } from './Member' import * as z from 'zod' import { RemoveRegularMember } from './RemoveRegularMember' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -36,6 +37,8 @@ export const RegularMember = ({ applicationId, }) + const { setValue } = useFormContext() + const handleMemberChange = ( value: string, key: keyof MemberProperties, @@ -84,6 +87,8 @@ export const RegularMember = ({ updatedRegularSignature, ) + setValue(InputFields.signature.regular, updatedRegularSignature) + return updatedSignatures } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx index 43d6f6ba4295..941aa82bf009 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx @@ -5,6 +5,7 @@ import { MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT } from '../../lib/constants' import set from 'lodash/set' import * as styles from './Signatures.css' import { getCommitteeAnswers, isCommitteeSignature } from '../../lib/utils' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -19,6 +20,8 @@ export const RemoveCommitteeMember = ({ applicationId, }) + const { setValue } = useFormContext() + const onRemoveMember = () => { const { currentAnswers, signature } = getCommitteeAnswers( application.answers, @@ -36,6 +39,8 @@ export const RemoveCommitteeMember = ({ updatedCommitteeSignature, ) + setValue(InputFields.signature.committee, updatedCommitteeSignature) + updateApplication(updatedAnswers) } } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx index bba44b95ff35..3cbf1b88cfac 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx @@ -5,6 +5,7 @@ import { MINIMUM_REGULAR_SIGNATURE_MEMBER_COUNT } from '../../lib/constants' import set from 'lodash/set' import * as styles from './Signatures.css' import { getRegularAnswers } from '../../lib/utils' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -21,6 +22,8 @@ export const RemoveRegularMember = ({ applicationId, }) + const { setValue } = useFormContext() + const onRemoveMember = () => { const { currentAnswers, signature } = getRegularAnswers(application.answers) @@ -45,6 +48,8 @@ export const RemoveRegularMember = ({ updatedRegularSignature, ) + setValue(InputFields.signature.regular, updatedRegularSignature) + updateApplication(updatedAnswers) } } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx index 27daca697399..d7734850ca84 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx @@ -4,6 +4,7 @@ import { getValueViaPath } from '@island.is/application/core' import { InputFields } from '../../lib/types' import { isRegularSignature } from '../../lib/utils' import set from 'lodash/set' +import { useFormContext } from 'react-hook-form' type Props = { applicationId: string @@ -18,6 +19,8 @@ export const RemoveRegularSignature = ({ applicationId, }) + const { setValue } = useFormContext() + const onRemove = () => { const currentAnswers = structuredClone(application.answers) const signature = getValueViaPath( @@ -36,6 +39,8 @@ export const RemoveRegularSignature = ({ updatedRegularSignature, ) + setValue(InputFields.signature.regular, updatedRegularSignature) + updateApplication(updatedSignatures) } } diff --git a/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx b/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx index d8dfe7783b50..d55719e6588e 100644 --- a/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx @@ -14,7 +14,7 @@ import { DEFAULT_COMMITTEE_SIGNATURE_MEMBER_COUNT, DEFAULT_REGULAR_SIGNATURE_COUNT, DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT, - OJOI_INPUT_HEIGHT as OJOI_INPUT_HEIGHT, + OJOI_INPUT_HEIGHT, SignatureTypes, } from '../lib/constants' import { useApplication } from '../hooks/useUpdateApplication' From 2366141f1e537147d4af67d97849a165f4d106aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:37:21 +0000 Subject: [PATCH 132/173] feat(web): Benefits of digital processes - Calculator (#16139) * Initial flow * Add formula to calculate results * Update visuals * Fix translations and remove unused config * Move functionality around * Add new result card * Add translations * Add scroll * Move setTimeout into useEffect * Make sure that calculations are hidden if user backspaces --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/components/Charts/v2/utils/format.ts | 4 +- .../BenefitsOfDigitalProcessesCalculator.tsx | 407 ++++++++++++++++++ .../translation.strings.ts | 167 +++++++ .../utils.ts | 124 ++++++ apps/web/utils/richText.tsx | 6 + 5 files changed, 706 insertions(+), 2 deletions(-) create mode 100644 apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx create mode 100644 apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts create mode 100644 apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts diff --git a/apps/web/components/Charts/v2/utils/format.ts b/apps/web/components/Charts/v2/utils/format.ts index f324f599f1df..275429276ed2 100644 --- a/apps/web/components/Charts/v2/utils/format.ts +++ b/apps/web/components/Charts/v2/utils/format.ts @@ -43,12 +43,12 @@ export const formatValueForPresentation = ( let divider = 1 let postfix = '' - let precision = 0 + let precision = increasePrecisionBy if (reduceAndRoundValue && value >= 1e6) { divider = 1e6 postfix = messages[activeLocale].millionPostfix - precision = 1 + increasePrecisionBy + precision += 1 } else if (reduceAndRoundValue && value >= 1e4) { divider = 1e3 postfix = messages[activeLocale].thousandPostfix diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx new file mode 100644 index 000000000000..196debea5fa6 --- /dev/null +++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx @@ -0,0 +1,407 @@ +import { + type PropsWithChildren, + type ReactNode, + useEffect, + useRef, + useState, +} from 'react' +import { useIntl } from 'react-intl' +import NumberFormat from 'react-number-format' + +import { + Box, + Button, + GridColumn, + GridRow, + Icon, + Inline, + Input, + Stack, + Text, +} from '@island.is/island-ui/core' +import type { SpanType } from '@island.is/island-ui/core/types' +import type { ConnectedComponent } from '@island.is/web/graphql/schema' +import { useI18n } from '@island.is/web/i18n' +import { formatCurrency } from '@island.is/web/utils/currency' + +import { formatValueForPresentation } from '../../Charts/v2/utils' +import { t } from './translation.strings' +import { calculateResults, UserInput } from './utils' + +interface FieldProps { + heading: string + description?: string +} + +const Field = ({ + heading, + description, + children, +}: PropsWithChildren) => { + return ( + + + {heading} + + {description && {description}} + {children} + + ) +} + +interface ResultCardProps { + title: string + icon?: ReactNode + description: string +} + +const ResultCard = ({ title, icon, description }: ResultCardProps) => { + return ( + + + + {icon && icon} + {title} + + + {description} + + + ) +} + +const canCalculate = (current: UserInput, previous: UserInput | null) => { + if ( + !( + current.nameOfProcess.length > 0 && + current.amountPerYear > 0 && + current.processDurationInMinutes > 0 && + current.visitCountToCompleteProcess > 0 && + current.averageDistanceToProcessInKilometers > 0 + ) + ) { + return false + } + + if (!previous) { + return true + } + + for (const key in current) { + if ( + current[key as keyof typeof current] !== + previous[key as keyof typeof current] + ) { + return true + } + } + + return false +} + +interface BenefitsOfDigitalProcessesCalculatorProps { + slice: ConnectedComponent +} + +export const BenefitsOfDigitalProcessesCalculator = ({ + slice, +}: BenefitsOfDigitalProcessesCalculatorProps) => { + const { formatMessage } = useIntl() + const { activeLocale } = useI18n() + + const resultsRef = useRef(null) + const [previousInput, setPreviousInput] = useState(null) + + const [userInput, setUserInput] = useState({ + amountPerYear: 0, + averageDistanceToProcessInKilometers: + slice.configJson?.['defaultAverageDistanceToProcessInKilometers'] ?? 7.5, + nameOfProcess: '', + processDurationInMinutes: 0, + visitCountToCompleteProcess: 0, + }) + + const resultColumnSpan: SpanType = ['1/1', '1/2', '1/1', '1/2', '1/3'] + + const { results, gainPerCitizen, ringRoadTripsSaved, co2 } = calculateResults( + slice, + userInput, + ) + + const displayResults = + Boolean(previousInput) && + !canCalculate(userInput, previousInput) && + userInput.nameOfProcess.length > 0 && + userInput.amountPerYear > 0 && + userInput.processDurationInMinutes > 0 && + userInput.visitCountToCompleteProcess > 0 && + userInput.averageDistanceToProcessInKilometers > 0 + + useEffect(() => { + if (!previousInput) return + resultsRef.current?.scrollIntoView({ + behavior: 'smooth', + }) + }, [previousInput]) + + return ( + + + + + { + setUserInput((prevInput) => ({ + ...prevInput, + nameOfProcess: ev.target.value, + })) + }} + label={formatMessage(t.nameOfProcess.label)} + placeholder={formatMessage(t.nameOfProcess.placeholder)} + /> + + + + { + setUserInput((prevInput) => ({ + ...prevInput, + amountPerYear: Number(value), + })) + }} + customInput={Input} + name="amountPerYear" + id="amountPerYear" + type="text" + inputMode="numeric" + thousandSeparator="." + decimalSeparator="," + label={formatMessage(t.amountPerYear.label)} + placeholder={formatMessage(t.amountPerYear.placeholder)} + /> + + + + { + setUserInput((prevInput) => ({ + ...prevInput, + processDurationInMinutes: Number(value), + })) + }} + customInput={Input} + name="processDurationInMinutes" + id="processDurationInMinutes" + type="text" + inputMode="numeric" + thousandSeparator="." + decimalSeparator="," + label={formatMessage(t.processDurationInMinutes.label)} + placeholder={formatMessage( + t.processDurationInMinutes.placeholder, + )} + /> + + + + { + setUserInput((prevInput) => ({ + ...prevInput, + visitCountToCompleteProcess: Number(value), + })) + }} + customInput={Input} + name="visitCountToCompleteProcess" + id="visitCountToCompleteProcess" + type="text" + inputMode="numeric" + thousandSeparator="." + decimalSeparator="," + label={formatMessage(t.visitCountToCompleteProcess.label)} + placeholder={formatMessage( + t.visitCountToCompleteProcess.placeholder, + )} + /> + + + + { + setUserInput((prevInput) => ({ + ...prevInput, + averageDistanceToProcessInKilometers: Number(value), + })) + }} + isNumericString={true} + customInput={Input} + name="averageDistanceToProcessInKilometers" + id="averageDistanceToProcessInKilometers" + inputMode="decimal" + thousandSeparator="." + decimalSeparator="," + label={formatMessage( + t.averageDistanceToProcessInKilometers.label, + )} + /> + + + + + + +
+ {displayResults && ( + + {userInput.nameOfProcess} + + + + = 1e6 + ? `${formatValueForPresentation( + activeLocale, + results.institutionGain, + )}${formatMessage(t.results.currencyPostfix)}` + : (formatCurrency( + results.institutionGain, + formatMessage(t.results.currencyPostfix), + ) as string) + } + description={formatMessage( + t.results.institutionGainDescription, + )} + icon={} + /> + + + } + /> + + + = 1e6 + ? `${formatValueForPresentation( + activeLocale, + gainPerCitizen, + )}${formatMessage(t.results.currencyPostfix)}` + : (formatCurrency( + gainPerCitizen, + formatMessage(t.results.currencyPostfix), + ) as string) + } + description={formatMessage(t.results.citizenGainDescription, { + nameOfProcess: userInput.nameOfProcess, + })} + icon={} + /> + + + } + /> + + + } + /> + + + = 1e6 + ? `${formatValueForPresentation( + activeLocale, + co2, + )}${formatMessage(t.results.kgPostfix)}` + : (formatCurrency( + co2, + formatMessage(t.results.kgPostfix), + ) as string) + } + description={formatMessage(t.results.c02)} + icon={ + + } + /> + + + + )} +
+
+ ) +} diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts new file mode 100644 index 000000000000..7f286a92036a --- /dev/null +++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts @@ -0,0 +1,167 @@ +import { defineMessages } from 'react-intl' + +export const t = { + nameOfProcess: defineMessages({ + heading: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:nameOfProcess.heading', + defaultMessage: 'Nafn ferils', + description: 'Heading á "nafn ferils" reit', + }, + label: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:nameOfProcess.label', + defaultMessage: 'Nafn ferils', + description: 'Label á "nafn ferils" reit', + }, + placeholder: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:nameOfProcess.placeholder', + defaultMessage: ' ', + description: 'Placeholder á "nafn ferils" reit', + }, + }), + amountPerYear: defineMessages({ + heading: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.heading', + defaultMessage: 'Magn á ári', + description: 'Heading á "Fjöldi afgreiðslna á ári" reit', + }, + label: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.label', + defaultMessage: 'Fjöldi afgreiðslna á ári', + description: 'Label á "magn á ári" reit', + }, + placeholder: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.placeholder', + defaultMessage: ' ', + description: 'Placeholder á "magn á ári" reit', + }, + description: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.description', + defaultMessage: 'Fjöldi afgreiðslna á ákveðinni þjónustu á einu ári', + description: 'Lýsing á "magn á ári" reit', + }, + }), + processDurationInMinutes: defineMessages({ + heading: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.heading', + defaultMessage: 'Lengd afgreiðslu í mínútum', + description: 'Heading á "Lengd afgreiðslu í mínútum" reit', + }, + label: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.label', + defaultMessage: 'Lengd afgreiðslu í mínútum', + description: 'Label á "Lengd afgreiðslu í mínútum" reit', + }, + placeholder: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.placeholder', + defaultMessage: ' ', + description: 'Placeholder á "Lengd afgreiðslu í mínútum"', + }, + description: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.description', + defaultMessage: + 'Áætluð lengd afgreiðslu. Biðtími þjónustuþega er ekki meðtalinn.', + description: 'Placeholder á "Lengd afgreiðslu í mínútum"', + }, + }), + visitCountToCompleteProcess: defineMessages({ + heading: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.heading', + defaultMessage: 'Fjöldi heimsókna', + description: 'Heading á "Fjöldi heimsókna" reit', + }, + label: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.label', + defaultMessage: 'Fjöldi heimsókna', + description: 'Label á "Fjöldi heimsókna" reit', + }, + placeholder: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.placeholder', + defaultMessage: ' ', + description: 'Placeholder á "Fjöldi heimsókna" reit', + }, + description: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.description', + defaultMessage: + 'Fjöldi heimsókna sem þarf til að ljúka afgreiðslu. Ef það þarf að mæta á staðinn til þess að sækja um og koma svo aftur til þess að sækja t.d. vottorð skal slá inn 2.', + description: 'Lýsing á "Fjöldi heimsókna" reit', + }, + }), + averageDistanceToProcessInKilometers: defineMessages({ + heading: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.heading', + defaultMessage: 'Lengd ferðar í kílómetrum', + description: 'Heading á "Lengd ferðar í kílómetrum" reit', + }, + label: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.label', + defaultMessage: 'Lengd ferðar í kílómetrum', + description: 'Label á "Lengd ferðar í kílómetrum" reit', + }, + placeholder: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.placeholder', + defaultMessage: ' ', + description: 'Placeholder á "Lengd ferðar í kílómetrum" reit', + }, + description: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.description', + defaultMessage: 'Áætluð meðalfjarlægð frá afgreiðslustöð.', + description: 'Lýsing á "Lengd ferðar í kílómetrum" reit', + }, + }), + results: defineMessages({ + calculate: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.calculate', + defaultMessage: 'Reikna', + description: 'Texti fyrir "Reikna" hnapp', + }, + institutionGainDescription: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.institutionGainDescription', + defaultMessage: 'árlegur fjárhagslegurstofnunar', + description: 'Lýsing á "ávinning stofnana" niðurstöðu', + }, + staffFreeToDoOtherThings: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.staffFreeToDoOtherThings', + defaultMessage: 'ígildi stöðugildi sem nýtast í önnur verkefni', + description: + 'Lýsing á "hve margir starfsmenn geta gert annað" niðurstöðu', + }, + citizenGainDescription: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.citizenGainDescription', + defaultMessage: 'heildarábati Íslands, ríki og borgara', + description: 'Lýsing á "ávinningur borgara" niðurstöðu', + }, + ringRoadTripsSaved: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.ringRoadTripsSaved', + defaultMessage: 'keyrðar ferðir í kringum Ísland sem sparast', + description: + 'Lýsing á "keyrðar ferðir í kringum Ísland sem sparast" niðurstöðu', + }, + savedCitizenDays: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.savedCitizenDays', + defaultMessage: + 'sparaðir hjá fólki við að sækja sér nauðsynlega þjónustu', + description: + 'Lýsing á "sparaðir dagar hjá fólki við að sækja sér nauðsynlega þjónustu" niðurstöðu', + }, + c02: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.c02', + defaultMessage: 'minni losun Co2 vegna færri bílferða', + description: 'Lýsing á "minni losun Co2 vegna færri bílferða" niðurstöðu', + }, + days: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.days', + defaultMessage: 'dagar', + description: 'Dagar', + }, + currencyPostfix: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.currencyPostfix', + defaultMessage: ' kr.', + description: 'Viðskeyti eftir krónutölu', + }, + kgPostfix: { + id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.kgPostfix', + defaultMessage: ' kg', + description: 'Viðskeyti eftir kílometratölu í niðurstöuspjöldum', + }, + }), +} diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts new file mode 100644 index 000000000000..fa9c4772f35e --- /dev/null +++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts @@ -0,0 +1,124 @@ +import type { ConnectedComponent } from '@island.is/web/graphql/schema' + +export interface UserInput { + nameOfProcess: string + amountPerYear: number + processDurationInMinutes: number + visitCountToCompleteProcess: number + averageDistanceToProcessInKilometers: number +} + +export interface Results { + /* Ávinningur stofnunar */ + institutionGain: number + + /* Ávinningur borgara */ + citizenGain: number + + /* Ígildi stöðugildis */ + staffFreeToDoOtherThings: number + + /* Eknir kílómetrar */ + drivenKilometersSaved: number + + /* Sparaðir dagar hjá fólki við að sækja sér þjónustu */ + citizenTimeSaved: number +} + +const avinningurR = ( + laun: number, + f2f: number, + lengd: number, + magn: number, +) => { + return (laun / 60) * f2f * lengd * magn +} + +const avinningurB = ( + fornarkostnadur: number, + lengd: number, + f2f: number, + km: number, + kmgjald: number, + okuhradi: number, + magn: number, +) => { + return ( + (f2f * 2 * km * kmgjald + + (60 / okuhradi) * km * 2 * f2f * (fornarkostnadur / 60) + + ((f2f * lengd) / 60) * fornarkostnadur) * + magn + ) +} + +export const calculateResults = ( + slice: ConnectedComponent, + userInput: UserInput, +) => { + const preConditions = { + staffIncomePerHour: + slice.configJson?.['Laun starfsmanna í framþjónustu krónur á klst'] ?? + 6010, + citizenIncomeLossPerHour: + slice.configJson?.[ + 'Fórnarkostnaður borgarar (meðallaun í landi á klst)' + ] ?? 5122, + kilometerFeePerKilometer: slice.configJson?.['Km gjald pr km'] ?? 141, + averageDrivingSpeedInKilometersPerHour: + slice.configJson?.['Meðalökuhraði km/klst'] ?? 40, + staffHourAverageInYear: + slice.configJson?.['Klukkustundir í stöðugildi á ári'] ?? 1606, + ringRoadDistanceInKilometers: + slice.configJson?.['Hringvegurinn í km'] ?? 1321, + kgCo2PerDrivenKilometer: slice.configJson?.['Kg co2 á ekinn km'] ?? 0.1082, + } + + const results: Results = { + institutionGain: avinningurR( + preConditions.staffIncomePerHour, + userInput.visitCountToCompleteProcess, + userInput.processDurationInMinutes, + userInput.amountPerYear, + ), + citizenGain: avinningurB( + preConditions.citizenIncomeLossPerHour, + userInput.processDurationInMinutes, + userInput.visitCountToCompleteProcess, + userInput.averageDistanceToProcessInKilometers, + preConditions.kilometerFeePerKilometer, + preConditions.averageDrivingSpeedInKilometersPerHour, + userInput.amountPerYear, + ), + staffFreeToDoOtherThings: + (userInput.amountPerYear * + userInput.processDurationInMinutes * + userInput.visitCountToCompleteProcess) / + 60 / + preConditions.staffHourAverageInYear, + drivenKilometersSaved: + userInput.amountPerYear * + userInput.visitCountToCompleteProcess * + 2 * + userInput.averageDistanceToProcessInKilometers, + citizenTimeSaved: + (((userInput.visitCountToCompleteProcess * + 2 * + userInput.averageDistanceToProcessInKilometers * + 60) / + preConditions.averageDrivingSpeedInKilometersPerHour + + userInput.visitCountToCompleteProcess * + userInput.processDurationInMinutes) * + userInput.amountPerYear) / + 60 / + 24, + } + + const gainPerCitizen = results.citizenGain + results.institutionGain + const ringRoadTripsSaved = + results.drivenKilometersSaved / preConditions.ringRoadDistanceInKilometers + + const co2 = + preConditions.kgCo2PerDrivenKilometer * results.drivenKilometersSaved + + return { results, gainPerCitizen, ringRoadTripsSaved, co2 } +} diff --git a/apps/web/utils/richText.tsx b/apps/web/utils/richText.tsx index 837fffe1f256..3701edfb4a4e 100644 --- a/apps/web/utils/richText.tsx +++ b/apps/web/utils/richText.tsx @@ -79,6 +79,7 @@ import { import { useI18n } from '@island.is/web/i18n' import AdministrationOfOccupationalSafetyAndHealthCourses from '../components/connected/AdministrationOfOccupationalSafetyAndHealthCourses/AdministrationOfOccupationalSafetyAndHealthCourses' +import { BenefitsOfDigitalProcessesCalculator } from '../components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator' import { MonthlyStatistics } from '../components/connected/electronicRegistrationStatistics' import { GrindavikResidentialPropertyPurchaseCalculator } from '../components/connected/GrindavikResidentialPropertyPurchaseCalculator' import HousingBenefitCalculator from '../components/connected/HousingBenefitCalculator/HousingBenefitCalculator/HousingBenefitCalculator' @@ -191,6 +192,11 @@ export const webRenderConnectedComponent = ( case 'VMST/ParentalLeaveCalculator': connectedComponent = break + case 'DigitalIceland/BenefitsOfDigitalProcesses': + connectedComponent = ( + + ) + break default: connectedComponent = renderConnectedComponent(slice) } From 59391ddefeb60aacbfc3ae3e01d18e4ad4018409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 25 Sep 2024 14:41:14 +0200 Subject: [PATCH 133/173] chore(j-s): Robot URLs (#16144) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../backend/src/app/modules/court/court.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/judicial-system/backend/src/app/modules/court/court.service.ts b/apps/judicial-system/backend/src/app/modules/court/court.service.ts index 78d154cc509b..208ff6752f6b 100644 --- a/apps/judicial-system/backend/src/app/modules/court/court.service.ts +++ b/apps/judicial-system/backend/src/app/modules/court/court.service.ts @@ -1,4 +1,5 @@ import formatISO from 'date-fns/formatISO' +import { Base64 } from 'js-base64' import { Sequelize } from 'sequelize-typescript' import { ConfidentialClientApplication } from '@azure/msal-node' @@ -840,7 +841,12 @@ export class CourtService { ): Promise { try { const subject = `Landsréttur - ${appealCaseNumber} - skjal` - const content = JSON.stringify({ category, name, dateSent, url }) + const content = JSON.stringify({ + category, + name, + dateSent, + url: url && Base64.encode(url), + }) return this.sendToRobot( subject, From f580a50b88cbc7938e991e563c86b52682b6b6f5 Mon Sep 17 00:00:00 2001 From: unakb Date: Wed, 25 Sep 2024 13:06:19 +0000 Subject: [PATCH 134/173] fix(j-s): Memoize CaseFiles so they don't move when renaming (#16149) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Indictments/CaseFile/CaseFile.tsx | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFile/CaseFile.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFile/CaseFile.tsx index 63cb249bd6d4..e86149181923 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFile/CaseFile.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/CaseFile/CaseFile.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useState } from 'react' +import { useCallback, useContext, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import { LayoutGroup } from 'framer-motion' import router from 'next/router' @@ -23,6 +23,15 @@ import { caseFile as m } from './CaseFile.strings' const CaseFile = () => { const { workingCase, isLoadingWorkingCase, caseNotFound } = useContext(FormContext) + + const caseFiles = useMemo(() => { + return ( + workingCase.caseFiles?.filter( + (caseFile) => caseFile.category === CaseFileCategory.CASE_FILE_RECORD, + ) ?? [] + ) + }, [workingCase.caseFiles]) + const { formatMessage } = useIntl() const [editCount, setEditCount] = useState(0) @@ -57,24 +66,20 @@ const CaseFile = () => { - {workingCase.policeCaseNumbers?.map((policeCaseNumber, index) => ( - - caseFile.policeCaseNumber === policeCaseNumber && - caseFile.category === CaseFileCategory.CASE_FILE_RECORD, - ) ?? [] - } - subtypes={workingCase.indictmentSubtypes} - crimeScenes={workingCase.crimeScenes} - setEditCount={setEditCount} - /> - ))} + {workingCase.policeCaseNumbers?.map((policeCaseNumber, index) => { + return ( + + ) + })} From 9e0fd7860030795d30e244fc2c6efe9579f7c91d Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:15:50 +0000 Subject: [PATCH 135/173] feat(web): Add default header for rettindagaesla fatlads folks organization (#16145) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../components/Organization/Wrapper/OrganizationWrapper.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 52b6baa452ed..af00b7732f18 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -532,7 +532,9 @@ export const OrganizationHeader: React.FC< /> ) case 'rettindagaesla-fatlads-folks': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Wed, 25 Sep 2024 13:27:43 +0000 Subject: [PATCH 136/173] fix(auth-admin): Fix ui issues (#16140) * fix alignment issues on root screen * fixes alignment issues on details screen * success toast on create and only allow future dates in datepicker * navigate to delegation list for from nationalId when create is successful * add appearInline to datepicker * remove comment * chore: nx format:write update dirty files * refactor --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../delegation-admin/src/lib/messages.ts | 8 ++- .../CreateDelegation.action.ts | 11 ++- .../CreateDelegation/CreateDelegation.tsx | 39 +++++++++- .../DelegationAdmin.tsx | 71 +++++++++---------- .../delegation-admin/src/screens/Root.tsx | 37 ++++++---- 5 files changed, 107 insertions(+), 59 deletions(-) diff --git a/libs/portals/admin/delegation-admin/src/lib/messages.ts b/libs/portals/admin/delegation-admin/src/lib/messages.ts index 4ce4b5e4638c..f6377eff0302 100644 --- a/libs/portals/admin/delegation-admin/src/lib/messages.ts +++ b/libs/portals/admin/delegation-admin/src/lib/messages.ts @@ -19,7 +19,7 @@ export const m = defineMessages({ }, createNewDelegation: { id: 'admin.delegationAdmin:delegationAdminCreateNewDelegation', - defaultMessage: 'Stofna nýtt umboð', + defaultMessage: 'Skrá nýtt umboð', }, delegationFrom: { id: 'admin.delegationAdmin:delegationAdminDelegationFrom', @@ -79,7 +79,7 @@ export const m = defineMessages({ }, referenceId: { id: 'admin.delegationAdmin:referenceId', - defaultMessage: 'Númer mála í Zendesk', + defaultMessage: 'Númer máls í Zendesk', }, errorDefault: { id: 'admin.delegationAdmin:errorDefault', @@ -117,4 +117,8 @@ export const m = defineMessages({ id: 'admin.delegationAdmin:createDelegationConfirmModalTitle', defaultMessage: 'Þú ert að skrá nýtt umboð', }, + createDelegationSuccessToast: { + id: 'admin.delegationAdmin:createDelegationSuccessToast', + defaultMessage: 'Umboð var skráð', + }, }) diff --git a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts index 9d85e6cf80ea..83aac0a774f7 100644 --- a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts +++ b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts @@ -1,7 +1,6 @@ import { z } from 'zod' import kennitala from 'kennitala' import isFuture from 'date-fns/isFuture' -import { redirect } from 'react-router-dom' import { WrappedActionFn } from '@island.is/portals/core' import { validateFormData, @@ -12,7 +11,6 @@ import { CreateDelegationMutation, CreateDelegationMutationVariables, } from './CreateDelegation.generated' -import { DelegationAdminPaths } from '../../lib/paths' const schema = z .object({ @@ -52,6 +50,7 @@ export type CreateDelegationResult = ValidateFormDataResult & { * Global error message if the mutation fails */ globalError?: boolean + success?: boolean } export const createDelegationAction: WrappedActionFn = @@ -83,8 +82,14 @@ export const createDelegationAction: WrappedActionFn = }, }) - return redirect(DelegationAdminPaths.Root) + return { + errors: null, + data: null, + globalError: false, + success: true, + } } catch (e) { + console.error(e) return { errors: null, data: null, diff --git a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx index 74adeb5e4355..c7ec091a643f 100644 --- a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx +++ b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx @@ -19,9 +19,10 @@ import { IntroHeader, m as coreMessages } from '@island.is/portals/core' import { m } from '../../lib/messages' import { DelegationAdminPaths } from '../../lib/paths' import NumberFormat from 'react-number-format' - +import startOfDay from 'date-fns/startOfDay' import { Form, + redirect, useActionData, useNavigate, useSearchParams, @@ -39,8 +40,9 @@ import { import { CreateDelegationConfirmModal } from '../../components/CreateDelegationConfirmModal' import { Identity } from '@island.is/api/schema' import kennitala from 'kennitala' -import { unmaskString } from '@island.is/shared/utils' +import { maskString, unmaskString } from '@island.is/shared/utils' import { useAuth } from '@island.is/auth/react' +import { replaceParams } from '@island.is/react-spa/shared' const CreateDelegationScreen = () => { const { formatMessage } = useLocale() @@ -72,7 +74,32 @@ const CreateDelegationScreen = () => { }, ] + async function success() { + try { + const maskedNationalId = await maskString( + fromIdentity?.nationalId ?? '', + userInfo?.profile.nationalId ?? '', + ) + successToast() + navigate( + replaceParams({ + href: DelegationAdminPaths.DelegationAdmin, + params: { + nationalId: maskedNationalId ?? '', + }, + }), + { replace: true }, + ) + } catch (e) { + navigate(DelegationAdminPaths.Root) + } + } + useEffect(() => { + if (actionData?.success) { + success() + } + if (actionData?.data && !actionData.errors) { setIsConfirmed(true) setShowConfirmModal(true) @@ -95,13 +122,17 @@ const CreateDelegationScreen = () => { } } - getFromNationalId() + !!defaultFromNationalId && getFromNationalId() }, [defaultFromNationalId]) const noUserFoundToast = () => { toast.warning(formatMessage(m.grantIdentityError)) } + const successToast = () => { + toast.success(formatMessage(m.createDelegationSuccessToast)) + } + const [getFromIdentity, { loading: fromIdentityQueryLoading }] = useIdentityLazyQuery({ onError: (error) => { @@ -339,6 +370,8 @@ const CreateDelegationScreen = () => { errorMessage={formatMessage( m[actionData?.errors?.validTo as keyof typeof m], )} + minDate={startOfDay(new Date())} + appearInline /> { return ( navigate(DelegationAdminPaths.Root)} /> - - - - + + {hasAdminAccess && ( - - + +
)} - + + { return ( <> - - - - + {hasAdminAccess && ( - - + +
)} - + + + +
Date: Wed, 25 Sep 2024 13:49:30 +0000 Subject: [PATCH 137/173] fix(signature-collection): Get candidate collectors (#16146) * get candidate collectors * add collectors to frontend * coderabbit suggestion --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/signatureCollection.resolver.ts | 16 ++++++++++++ .../src/lib/signatureCollection.service.ts | 15 +++++++++++ .../src/lib/signature-collection.service.ts | 19 ++++++++++++++ .../src/hooks/graphql/queries.ts | 9 +++++++ .../signature-collection/src/hooks/index.ts | 11 ++++++++ .../screens/Parliamentary/OwnerView/index.tsx | 26 ++++++++++++++----- .../signature-collection/src/skeletons.tsx | 6 +++++ 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts index a6edb5d5df50..4b27ba5a6ec9 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts @@ -34,6 +34,8 @@ import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper import { SignatureCollectionAddListsInput } from './dto/addLists.input' import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input' import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input' +import { SignatureCollectionCollector } from './models/collector.model' +import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input' @UseGuards(IdsUserGuard, ScopesGuard, UserAccessGuard) @Resolver() @Audit({ namespace: '@island.is/api/signature-collection' }) @@ -186,4 +188,18 @@ export class SignatureCollectionResolver { user, ) } + + @Scopes(ApiScope.signatureCollection) + @AccessRequirement(OwnerAccess.AllowActor) + @Query(() => [SignatureCollectionCollector]) + @Audit() + async signatureCollectionCollectors( + @CurrentUser() user: User, + @CurrentSignee() signee: SignatureCollectionSignee, + ): Promise { + return this.signatureCollectionService.collectors( + user, + signee.candidate?.id, + ) + } } diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts index a3ef87c27516..62fe983282a1 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts @@ -19,6 +19,8 @@ import { SignatureCollectionOwnerInput } from './dto/owner.input' import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input' import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input' import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input' +import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input' +import { SignatureCollectionCollector } from './models/collector.model' @Injectable() export class SignatureCollectionService { @@ -161,4 +163,17 @@ export class SignatureCollectionService { return canSign && list.area.id === signee.area?.id } + + async collectors( + user: User, + candidateId: string | undefined, + ): Promise { + if (!candidateId) { + return [] + } + return await this.signatureCollectionClientService.getCollectors( + user, + candidateId, + ) + } } diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.ts index c2e1df76630a..2fc5e1b67d4d 100644 --- a/libs/clients/signature-collection/src/lib/signature-collection.service.ts +++ b/libs/clients/signature-collection/src/lib/signature-collection.service.ts @@ -524,4 +524,23 @@ export class SignatureCollectionClientService { } return { success: true } } + + async getCollectors( + auth: User, + candidateId: string, + ): Promise<{ name: string; nationalId: string }[]> { + const candidate = await this.getApiWithAuth( + this.candidateApi, + auth, + ).frambodIDGet({ + iD: parseInt(candidateId), + }) + + return ( + candidate.umbodList?.map((u) => ({ + name: u.nafn ?? '', + nationalId: u.kennitala ?? '', + })) ?? [] + ) + } } 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 4eae87f09acd..a9903bf9edb8 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts @@ -163,3 +163,12 @@ export const GetCanSign = gql` signatureCollectionCanSignFromPaper(input: $input) } ` + +export const GetCollectors = gql` + query SignatureCollectionCollectors { + signatureCollectionCollectors { + nationalId + name + } + } +` diff --git a/libs/service-portal/signature-collection/src/hooks/index.ts b/libs/service-portal/signature-collection/src/hooks/index.ts index 1c26cfd03b61..5dc621198d87 100644 --- a/libs/service-portal/signature-collection/src/hooks/index.ts +++ b/libs/service-portal/signature-collection/src/hooks/index.ts @@ -8,6 +8,7 @@ import { GetListsForOwner, GetCurrentCollection, GetCanSign, + GetCollectors, } from './graphql/queries' import { SignatureCollectionListBase, @@ -16,6 +17,7 @@ import { SignatureCollectionSuccess, SignatureCollection, SignatureCollectionSignedList, + SignatureCollectionCollector, } from '@island.is/api/schema' export const useGetSignatureList = (listId: string) => { @@ -169,3 +171,12 @@ export const useGetCanSign = ( const canSign = getCanSignData?.signatureCollectionCanSignFromPaper ?? false return { canSign, loadingCanSign } } + +export const useGetCollectors = () => { + const { data: getCollectorsData, loading: loadingCollectors } = + useQuery(GetCollectors) + const collectors = + (getCollectorsData?.signatureCollectionCollectors as SignatureCollectionCollector[]) ?? + [] + return { collectors, loadingCollectors } +} 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 659882231e88..4b7f5a3649ba 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 @@ -20,8 +20,11 @@ import { SignatureCollectionList, SignatureCollectionSuccess, } from '@island.is/api/schema' -import { OwnerParliamentarySkeleton } from '../../../skeletons' -import { useGetListsForOwner } from '../../../hooks' +import { + CollectorSkeleton, + OwnerParliamentarySkeleton, +} from '../../../skeletons' +import { useGetCollectors, useGetListsForOwner } from '../../../hooks' import { SignatureCollection } from '@island.is/api/schema' import { useMutation } from '@apollo/client' import { cancelCollectionMutation } from '../../../hooks/graphql/mutations' @@ -36,6 +39,7 @@ const OwnerView = ({ const { formatMessage } = useLocale() const { listsForOwner, loadingOwnerLists, refetchListsForOwner } = useGetListsForOwner(currentCollection?.id || '') + const { collectors, loadingCollectors } = useGetCollectors() const [cancelCollection] = useMutation( cancelCollectionMutation, @@ -179,10 +183,20 @@ const OwnerView = ({ - - {'Nafni Nafnason'} - {'010130-3019'} - + {loadingCollectors ? ( + + + + + + ) : ( + collectors.map((collector) => ( + + {collector.name} + {collector.nationalId} + + )) + )} diff --git a/libs/service-portal/signature-collection/src/skeletons.tsx b/libs/service-portal/signature-collection/src/skeletons.tsx index 04982b35c517..5c5d19d7387d 100644 --- a/libs/service-portal/signature-collection/src/skeletons.tsx +++ b/libs/service-portal/signature-collection/src/skeletons.tsx @@ -21,3 +21,9 @@ export const SkeletonTable = () => { ) } + +export const CollectorSkeleton = () => { + return ( + + ) +} From e5c74676363d8437f0e376781b36fa5379f540ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Wed, 25 Sep 2024 14:03:58 +0000 Subject: [PATCH 138/173] fix(regulations-admin): Minor fixes for affected (#16143) * Minor affeced bugfixes * Update after cr comments * Update article names --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/utils/formatAmendingRegulation.ts | 59 ++++++------ .../src/utils/formatAmendingUtils.ts | 89 +++++++++++++++++++ .../src/utils/groupByArticleTitle.ts | 24 ----- 3 files changed, 114 insertions(+), 58 deletions(-) create mode 100644 libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts delete mode 100644 libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts diff --git a/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts b/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts index ac3fca2aef41..884527fd26c2 100644 --- a/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts +++ b/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts @@ -5,7 +5,14 @@ import is from 'date-fns/locale/is' import compact from 'lodash/compact' import flatten from 'lodash/flatten' import uniq from 'lodash/uniq' -import { groupElementsByArticleTitleFromDiv } from './groupByArticleTitle' +import { + allSameDay, + extractArticleTitleDisplay, + getTextWithSpaces, + groupElementsByArticleTitleFromDiv, + isGildisTaka, + removeRegPrefix, +} from './formatAmendingUtils' import { getDeletionOrAddition } from './getDeletionOrAddition' // ---------------------------------------------------------------------- @@ -13,24 +20,12 @@ const PREFIX = 'Reglugerð um ' const PREFIX_AMENDING = 'breytingu á reglugerð nr. ' const PREFIX_REPEALING = 'brottfellingu á reglugerð nr. ' -const removeRegPrefix = (title: string) => { - if (/^Reglugerð/.test(title)) { - return title.replace(/^Reglugerð/, '') - } - return title -} - -const isGildisTaka = (str: string) => { - return /(öðlast|tekur).*gildi|sett.*með.*(?:heimild|stoð)/.test( - (str || '').toLowerCase(), - ) -} - const formatAffectedAndPlaceAffectedAtEnd = ( groups: { formattedRegBody: HTMLText[] date?: Date | undefined }[], + hideAffected?: boolean, ) => { function formatArray(arr: string[]): string { if (arr.length === 1) { @@ -115,7 +110,10 @@ const formatAffectedAndPlaceAffectedAtEnd = ( }) const uniqueGildistaka = uniq(gildsTakaKeepArray) - const joinedAffected = updatedImpactAffectArray.join('. ') + let joinedAffected = updatedImpactAffectArray.join('. ') + if (hideAffected) { + joinedAffected = '' + } const gildistakaReturn = flatten([...uniqueGildistaka, joinedAffected]).join( '', ) as HTMLText @@ -178,11 +176,11 @@ export const formatAmendingRegBody = ( ) => { const regName = removeRegNamePrefix(name) if (repeal) { - const title = regTitle ? regTitle.replace(/^reglugerð\s*/i, '') + ' ' : '' + const title = regTitle ? regTitle.replace(/^reglugerð\s*/i, '').trim() : '' const text = `

Reglugerð nr. ${regName} ${title.replace( /\.$/, '', - )}fellur brott.

` as HTMLText + )} fellur brott.

` as HTMLText const gildistaka = `

Reglugerð þessi er sett með heimild í [].

Reglugerðin öðlast þegar gildi.

` as HTMLText return [text, gildistaka] @@ -232,18 +230,8 @@ export const formatAmendingRegBody = ( if (element.classList.contains('article__title')) { const clone = element.cloneNode(true) - if (clone instanceof Element) { - const emElement = clone.querySelector('em') - if (emElement) { - emElement.parentNode?.removeChild(emElement) - } - - const textContent = clone.textContent?.trim() ?? '' - - articleTitle = textContent - } else { - articleTitle = element.innerText - } + const textContent = getTextWithSpaces(clone) + articleTitle = extractArticleTitleDisplay(textContent) testGroup.title = articleTitle isArticleTitle = true paragraph = 0 // Reset paragraph count for the new article @@ -385,10 +373,8 @@ export const formatAmendingRegBody = ( if (testGroup.isDeletion === true) { const articleTitleNumber = testGroup.title - const grMatch = articleTitleNumber.match(/^\d+\. gr\./) - const articleTitleDisplay = grMatch ? grMatch[0] : articleTitleNumber additionArray.push([ - `

${articleTitleDisplay} ${regNameDisplay} fellur brott.

` as HTMLText, + `

${articleTitleNumber} ${regNameDisplay} fellur brott.

` as HTMLText, ]) } else if (testGroup.isAddition === true) { let prevArticleTitle = '' @@ -401,7 +387,8 @@ export const formatAmendingRegBody = ( ? flatten(testGroup.original) : [] - const prevArticleTitleNumber = prevArticleTitle.match(/^\d+\. gr\./) + const prevArticleTitleNumber = + extractArticleTitleDisplay(prevArticleTitle) let articleDisplayText = '' @@ -449,7 +436,11 @@ export const formatAmendingBodyWithArticlePrefix = ( const additions = flatten(impactAdditionArray) - const htmlForEditor = formatAffectedAndPlaceAffectedAtEnd(additions) + const hideAffected = allSameDay(additions) + const htmlForEditor = formatAffectedAndPlaceAffectedAtEnd( + additions, + hideAffected, + ) const returnArray = compact(htmlForEditor) diff --git a/libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts b/libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts new file mode 100644 index 000000000000..007246a3332a --- /dev/null +++ b/libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts @@ -0,0 +1,89 @@ +import isSameDay from 'date-fns/isSameDay' +import { HTMLText } from '@island.is/regulations' + +export const groupElementsByArticleTitleFromDiv = ( + div: HTMLDivElement, +): HTMLElement[][] => { + const result: HTMLElement[][] = [] + let currentGroup: HTMLElement[] = [] + + Array.from(div.children).forEach((child) => { + const element = child as HTMLElement + if ( + element.classList.contains('article__title') || + element.classList.contains('chapter__title') + ) { + if (currentGroup.length > 0) { + result.push(currentGroup) + } + currentGroup = [element] + } else { + currentGroup.push(element) + } + }) + + if (currentGroup.length > 0) { + result.push(currentGroup) + } + + return result +} + +/** + * Extracts article title number (e.g., '1. gr.' or '1. gr. a') from a string, allowing for Icelandic characters. + */ +export const extractArticleTitleDisplay = (title: string): string => { + const grMatch = title.match(/^\d+\. gr\.(?: [\p{L}])?(?= |$)/u) + const articleTitleDisplay = grMatch ? grMatch[0] : title + return articleTitleDisplay +} + +export const getTextWithSpaces = (element: Node): string => { + let result = '' + + element.childNodes.forEach((node, index) => { + if (node.nodeType === Node.TEXT_NODE) { + result += (node.textContent?.trim() || '') + ' ' + } else if (node.nodeType === Node.ELEMENT_NODE) { + result += getTextWithSpaces(node as HTMLElement) + + // If the current element is not the last node and the next node is also an element or text node, + // add a space between elements + if ( + element.childNodes[index + 1] && + element.childNodes[index + 1].nodeType !== Node.COMMENT_NODE + ) { + result += ' ' + } + } + }) + + return result.trim() // Trim any excess space +} + +export const removeRegPrefix = (title: string) => { + if (/^Reglugerð/.test(title)) { + return title.replace(/^Reglugerð/, '') + } + return title +} + +export const isGildisTaka = (str: string) => { + return /(öðlast|tekur).*gildi|sett.*með.*(?:heimild|stoð)/.test( + (str || '').toLowerCase(), + ) +} + +export type AdditionObject = { + formattedRegBody: HTMLText[] + date: Date | undefined +} + +export const allSameDay = (objects: AdditionObject[]): boolean => { + const validObjects = objects.filter((obj) => obj.date !== undefined) + + if (validObjects.length === 0) return true + const firstDate = validObjects[0].date! + + return validObjects.every((obj) => isSameDay(obj.date!, firstDate)) +} diff --git a/libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts b/libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts deleted file mode 100644 index 96e064372852..000000000000 --- a/libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const groupElementsByArticleTitleFromDiv = ( - div: HTMLDivElement, -): HTMLElement[][] => { - const result: HTMLElement[][] = [] - let currentGroup: HTMLElement[] = [] - - Array.from(div.children).forEach((child) => { - const element = child as HTMLElement - if (element.classList.contains('article__title')) { - if (currentGroup.length > 0) { - result.push(currentGroup) - } - currentGroup = [element] - } else { - currentGroup.push(element) - } - }) - - if (currentGroup.length > 0) { - result.push(currentGroup) - } - - return result -} From abd67fa4f2deb8a8b455ca69cee82a8a529c304b Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:40:22 +0000 Subject: [PATCH 139/173] fix(signature-collection): sign list without candidateid (#16154) * fix(signature-collection): sign list without candidateid * temp * tweak * chore: nx format:write update dirty files --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/forms/Draft.ts | 88 ++++++++++++++----- .../src/forms/Prerequisites.ts | 5 -- .../src/lib/dataSchema.ts | 1 - .../src/lib/messages.ts | 20 +++++ .../src/lib/signListTemplate.ts | 7 +- .../screens/Parliamentary/OwnerView/index.tsx | 14 +-- .../signature-collection/src/skeletons.tsx | 4 +- 7 files changed, 97 insertions(+), 42 deletions(-) diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts index 0ffa0e7feb88..ac12c5c49250 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts @@ -1,8 +1,8 @@ import { buildDescriptionField, buildForm, - buildHiddenInput, buildMultiField, + buildRadioField, buildSection, buildSubmitField, buildTextField, @@ -20,7 +20,7 @@ export const Draft: Form = buildForm({ title: '', mode: FormModes.DRAFT, renderLastScreenButton: true, - renderLastScreenBackButton: true, + renderLastScreenBackButton: false, logo: Logo, children: [ buildSection({ @@ -33,6 +33,60 @@ export const Draft: Form = buildForm({ title: m.dataCollection, children: [], }), + /* section used for testing purposes */ + buildSection({ + id: 'selectCandidateSection', + title: m.selectCandidate, + condition: (_, externalData) => { + const lists = getValueViaPath( + externalData, + 'getList.data', + [], + ) as SignatureCollectionList[] + return lists.length > 1 + }, + children: [ + buildMultiField({ + id: 'selectCandidateSection', + title: m.selectCandidate, + description: m.selectCandidateDescription, + children: [ + buildRadioField({ + id: 'listId', + title: '', + defaultValue: '', + required: true, + options: ({ + externalData: { + getList: { data }, + }, + }) => { + return (data as SignatureCollectionList[]).map((list) => ({ + value: list.id, + label: list.candidate.name, + disabled: + list.maxReached || new Date(list.endTime) < new Date(), + tag: list.maxReached + ? { + label: m.selectCandidateMaxReached.defaultMessage, + variant: 'red', + outlined: true, + } + : new Date(list.endTime) < new Date() + ? { + label: m.selectCandidateListExpired.defaultMessage, + variant: 'red', + outlined: true, + } + : undefined, + })) + }, + }), + ], + }), + ], + }), + /* ------------------------------- */ buildSection({ id: 'signListInformationSection', title: m.information, @@ -47,24 +101,7 @@ export const Draft: Form = buildForm({ title: m.listHeader, titleVariant: 'h3', }), - buildHiddenInput({ - id: 'listId', - defaultValue: ({ answers, externalData }: Application) => { - const lists = getValueViaPath( - externalData, - 'getList.data', - [], - ) as SignatureCollectionList[] - const initialQuery = getValueViaPath( - answers, - 'initialQuery', - '', - ) - - return lists.find((x) => x.candidate.id === initialQuery)?.id - }, - }), buildTextField({ id: 'list.name', title: m.listName, @@ -83,7 +120,11 @@ export const Draft: Form = buildForm({ '', ) - return lists.find((x) => x.candidate.id === initialQuery)?.title + return lists.find((list) => + initialQuery + ? list.candidate.id === initialQuery + : list.id === answers.listId, + )?.candidate?.name }, }), buildTextField({ @@ -104,8 +145,11 @@ export const Draft: Form = buildForm({ '', ) - return lists.find((x) => x.candidate.id === initialQuery) - ?.candidate?.partyBallotLetter + return lists.find((list) => + initialQuery + ? list.candidate.id === initialQuery + : list.id === answers.listId, + )?.candidate?.partyBallotLetter }, }), buildDescriptionField({ diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts index 88e64f32721f..2455bc31fe78 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts @@ -87,11 +87,6 @@ export const Prerequisites: Form = buildForm({ title: '', subTitle: '', }), - buildDataProviderItem({ - //provider: TODO: Add providers needed for signing collection, - title: '', - subTitle: '', - }), ], }), ], diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts index 212151c18bb1..b7f38ebfe60d 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts @@ -3,7 +3,6 @@ import { z } from 'zod' export const dataSchema = z.object({ /* Gagnaöflun */ approveExternalData: z.boolean().refine((v) => v), - listId: z.string().min(1), list: z.object({ name: z.string(), letter: z.string(), diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts index 6800b483c490..ff0e5a3a8eef 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts @@ -101,6 +101,26 @@ export const m = defineMessages({ defaultMessage: 'Nafn', description: '', }, + selectCandidate: { + id: 'pls.application:selectCandidate', + defaultMessage: 'Veldu frambjóðanda', + description: '', + }, + selectCandidateDescription: { + id: 'pls.application:selectCandidateDescription', + defaultMessage: 'Frambjóðendur á þínu svæði sem hægt er að mæla með:', + description: '', + }, + selectCandidateMaxReached: { + id: 'pls.application:selectCandidateMaxReached', + defaultMessage: 'Hámarki meðmæla náð', + description: '', + }, + selectCandidateListExpired: { + id: 'pls.application:selectCandidateListExpired', + defaultMessage: 'Söfnuninni lokið', + description: '', + }, listName: { id: 'pls.application:listName', defaultMessage: 'Heiti framboðs', diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts index aba07ec736b7..8c385841626b 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts @@ -17,12 +17,7 @@ import { dataSchema } from './dataSchema' import { m } from './messages' import { EphemeralStateLifeCycle } from '@island.is/application/core' import { Features } from '@island.is/feature-flags' -import { - CanSignApi, - CurrentCollectionApi, - GetListApi, - OwnerRequirementsApi, -} from '../dataProviders' +import { CanSignApi, CurrentCollectionApi, GetListApi } from '../dataProviders' const WeekLifeCycle: StateLifeCycle = { shouldBeListed: false, 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 4b7f5a3649ba..541665057dbc 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 @@ -29,6 +29,7 @@ import { SignatureCollection } from '@island.is/api/schema' import { useMutation } from '@apollo/client' import { cancelCollectionMutation } from '../../../hooks/graphql/mutations' import copyToClipboard from 'copy-to-clipboard' +import { formatNationalId } from '@island.is/portals/core' const OwnerView = ({ currentCollection, @@ -88,7 +89,7 @@ const OwnerView = ({ )} {loadingOwnerLists ? ( - + ) : ( @@ -185,15 +186,18 @@ const OwnerView = ({ {loadingCollectors ? ( - - + + + + + ) : ( collectors.map((collector) => ( - {collector.name} - {collector.nationalId} + {collector.name} + {formatNationalId(collector.nationalId)} )) )} diff --git a/libs/service-portal/signature-collection/src/skeletons.tsx b/libs/service-portal/signature-collection/src/skeletons.tsx index 5c5d19d7387d..3b1ab3291fa3 100644 --- a/libs/service-portal/signature-collection/src/skeletons.tsx +++ b/libs/service-portal/signature-collection/src/skeletons.tsx @@ -23,7 +23,5 @@ export const SkeletonTable = () => { } export const CollectorSkeleton = () => { - return ( - - ) + return } From 3a2a05f3d238ce6c331c94d334acffc964c406cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:20:21 +0000 Subject: [PATCH 140/173] feat(web): Leaf icon (#16152) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../BenefitsOfDigitalProcessesCalculator.tsx | 4 +-- .../translation.strings.ts | 4 +-- libs/island-ui/core/src/lib/IconRC/iconMap.ts | 3 ++ .../core/src/lib/IconRC/icons/Leaf.tsx | 22 ++++++++++++++ .../core/src/lib/IconRC/icons/LeafOutline.tsx | 29 +++++++++++++++++++ 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx create mode 100644 libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx index 196debea5fa6..58b869a9be66 100644 --- a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx +++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator.tsx @@ -393,9 +393,7 @@ export const BenefitsOfDigitalProcessesCalculator = ({ ) as string) } description={formatMessage(t.results.c02)} - icon={ - - } + icon={} /> diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts index 7f286a92036a..553e4111e9ff 100644 --- a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts +++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts @@ -60,7 +60,7 @@ export const t = { id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.description', defaultMessage: 'Áætluð lengd afgreiðslu. Biðtími þjónustuþega er ekki meðtalinn.', - description: 'Placeholder á "Lengd afgreiðslu í mínútum"', + description: 'Lýsing á "Lengd afgreiðslu í mínútum"', }, }), visitCountToCompleteProcess: defineMessages({ @@ -116,7 +116,7 @@ export const t = { }, institutionGainDescription: { id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.institutionGainDescription', - defaultMessage: 'árlegur fjárhagslegurstofnunar', + defaultMessage: 'árlegur fjárhagslegur ávinningur stofnunar', description: 'Lýsing á "ávinning stofnana" niðurstöðu', }, staffFreeToDoOtherThings: { diff --git a/libs/island-ui/core/src/lib/IconRC/iconMap.ts b/libs/island-ui/core/src/lib/IconRC/iconMap.ts index 5d92e500e1f1..e3e549ffa12b 100644 --- a/libs/island-ui/core/src/lib/IconRC/iconMap.ts +++ b/libs/island-ui/core/src/lib/IconRC/iconMap.ts @@ -90,6 +90,7 @@ export type Icon = | 'swapVertical' | 'thumbsUp' | 'thumbsDown' + | 'leaf' export default { filled: { @@ -183,6 +184,7 @@ export default { swapVertical: 'SwapVertical', thumbsUp: 'ThumbsUp', thumbsDown: 'ThumbsDown', + leaf: 'Leaf', }, outline: { archive: 'ArchiveOutline', @@ -275,5 +277,6 @@ export default { swapVertical: 'SwapVertical', thumbsUp: 'ThumbsUpOutline', thumbsDown: 'ThumbsDownOutline', + leaf: 'LeafOutline', }, } diff --git a/libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx b/libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx new file mode 100644 index 000000000000..317a37b0ff6c --- /dev/null +++ b/libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import type { SvgProps as SVGRProps } from '../types' + +const Leaf = ({ + title, + titleId, + ...props +}: React.SVGProps & SVGRProps) => { + return ( + + {title ? {title} : null} + + + ) +} + +export default Leaf diff --git a/libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx b/libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx new file mode 100644 index 000000000000..717606a049d7 --- /dev/null +++ b/libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import type { SvgProps as SVGRProps } from '../types' + +const LeafOutline = ({ + title, + titleId, + ...props +}: React.SVGProps & SVGRProps) => { + return ( + + {title ? {title} : null} + + + ) +} + +export default LeafOutline From 33c68ca72055d2db4aa2e0e7925fa7bbba2bf57f Mon Sep 17 00:00:00 2001 From: birkirkristmunds <142495885+birkirkristmunds@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:59:48 +0000 Subject: [PATCH 141/173] chore(skilavottord): Inspect why traffic endpoint is not always returning data (#16151) - Added loggers - make sure Traffic is only called when needed Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../DeregisterVehicle/Confirm/Confirm.tsx | 10 ++++ .../samgongustofa/samgongustofa.service.ts | 56 ++++++++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx index 53333b718288..7c4cda33f594 100644 --- a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx +++ b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx @@ -101,6 +101,10 @@ const UpdateSkilavottordVehicleInfoMutation = gql` const Confirm: FC> = () => { const [reloadFlag, setReloadFlag] = useState(false) + const [ + vehicleReadyToDeregisteredQueryCompleted, + setVehicleReadyToDeregisteredQueryCompleted, + ] = useState(false) // Update reloadFlag to trigger the child component to reload const triggerReload = () => { @@ -134,6 +138,11 @@ const Confirm: FC> = () => { SkilavottordVehicleReadyToDeregisteredQuery, { variables: { permno: id }, + onCompleted: (data) => { + if (data && data.skilavottordVehicleReadyToDeregistered) { + setVehicleReadyToDeregisteredQueryCompleted(true) + } + }, }, ) @@ -143,6 +152,7 @@ const Confirm: FC> = () => { SkilavottordTrafficQuery, { variables: { permno: id }, + skip: !vehicleReadyToDeregisteredQueryCompleted, }, ) diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts index 58bd1fc96afd..caac3a135fda 100644 --- a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts @@ -347,23 +347,49 @@ export class SamgongustofaService { ) if (result.status === 200) { - // Get the latest registered traffic data - const traffic = Object.values(result.data).reduce( - (prev: Traffic, current: Traffic) => - new Date(prev.useDate) > new Date(current.useDate) ? prev : current, - {} as Traffic, - ) as Traffic - - logger.info( - `car-recycling: Got traffic data for ${getShortPermno(permno)}`, - { - outInStatus: traffic.outInStatus, - useStatus: traffic.useStatus, - useStatusName: traffic.useStatusName, - }, + if (result.data.length) { + // Get the latest registered traffic data + const traffic = Object.values(result.data).reduce( + (prev: Traffic, current: Traffic) => + new Date(prev.useDate) > new Date(current.useDate) + ? prev + : current, + {} as Traffic, + ) as Traffic + + logger.info( + `car-recycling: Got traffic data for ${getShortPermno(permno)}`, + { + permno: getShortPermno(traffic.permno), + outInStatus: traffic.outInStatus, + useStatus: traffic.useStatus, + useStatusName: traffic.useStatusName, + }, + ) + + // + if (!traffic.outInStatus) { + logger.warn( + `car-recycling: No traffic data being returned for ${getShortPermno( + permno, + )}`, + { dataFromServer: result.data }, + ) + } + + return traffic + } + + logger.warn( + `car-recycling: No traffic data found for ${getShortPermno(permno)}`, ) - return traffic + return { + permno, + outInStatus: '', + useStatus: '', + useStatusName: '', + } as Traffic } throw new Error( From e4b2b477c6def0bb63b7f899b9f6de7d9e25d495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:32:03 +0000 Subject: [PATCH 142/173] fix(cms): Ignore getOrganization call if no slug is provided (#16157) * Reduce include depth * Ignore if no slug provided * Only fetch one --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/cms/src/lib/cms.contentful.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/cms/src/lib/cms.contentful.service.ts b/libs/cms/src/lib/cms.contentful.service.ts index 7b48437abb7a..55f256689fce 100644 --- a/libs/cms/src/lib/cms.contentful.service.ts +++ b/libs/cms/src/lib/cms.contentful.service.ts @@ -310,11 +310,19 @@ export class CmsContentfulService { ) } - async getOrganization(slug: string, lang: string): Promise { + async getOrganization( + slug: string, + lang: string, + ): Promise { + if (!slug) { + return null + } + const params = { ['content_type']: 'organization', include: 10, 'fields.slug': slug, + limit: 1, } const result = await this.contentfulRepository From 6cc2dd6712d0d9e7908e3ba8b03526b39a47e31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 25 Sep 2024 19:35:08 +0200 Subject: [PATCH 143/173] chore(j-s): Arraignment Date Lock (#16153) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../routes/Court/Indictments/Subpoena/Subpoena.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx index 92455ec72667..4d13fb753f1c 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx @@ -46,11 +46,11 @@ const Subpoena: FC = () => { } = useCourtArrangements(workingCase, setWorkingCase, 'arraignmentDate') const { sendNotification } = useCase() - const isArraignmentDone = Boolean(workingCase.indictmentDecision) + const isArraignmentScheduled = Boolean(workingCase.arraignmentDate) const handleNavigationTo = useCallback( async (destination: keyof stepValidationsType) => { - if (isArraignmentDone) { + if (isArraignmentScheduled) { router.push(`${destination}/${workingCase.id}`) return } @@ -89,7 +89,7 @@ const Subpoena: FC = () => { router.push(`${destination}/${workingCase.id}`) }, [ - isArraignmentDone, + isArraignmentScheduled, sendCourtDateToServer, workingCase.defendants, workingCase.notifications, @@ -134,8 +134,8 @@ const Subpoena: FC = () => { handleCourtDateChange={handleCourtDateChange} handleCourtRoomChange={handleCourtRoomChange} courtDate={workingCase.arraignmentDate} - dateTimeDisabled={isArraignmentDone} - courtRoomDisabled={isArraignmentDone} + dateTimeDisabled={isArraignmentScheduled} + courtRoomDisabled={isArraignmentScheduled} courtRoomRequired /> @@ -169,14 +169,14 @@ const Subpoena: FC = () => { previousUrl={`${constants.INDICTMENTS_RECEPTION_AND_ASSIGNMENT_ROUTE}/${workingCase.id}`} nextIsLoading={isLoadingWorkingCase} onNextButtonClick={() => { - if (isArraignmentDone) { + if (isArraignmentScheduled) { router.push( `${constants.INDICTMENTS_DEFENDER_ROUTE}/${workingCase.id}`, ) } else setNavigateTo(constants.INDICTMENTS_DEFENDER_ROUTE) }} nextButtonText={ - isArraignmentDone + isArraignmentScheduled ? undefined : formatMessage(strings.nextButtonText) } From 651f3377f2855851debbb95e480ffa0f4346f19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20Gu=C3=B0mundsson?= <34029342+GunnlaugurG@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:14:38 +0000 Subject: [PATCH 144/173] feat(auth-admin): Create paper delegation (#15992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * adds getTicket function to Zendesk service Fetches Zendesk ticket by id * Adds create delegation route Form for creating delegation, wip form action * chore: nx format:write update dirty files * fix config value * chore: charts update dirty files * fix api build issues * wip gql for create delegation * fix pr comments * delegation reference id added * added back the spec files * validate form data and show error messages * fix get tests * chore: nx format:write update dirty files * test for delete * post method to create delegation between two national id's * zendesk integration complete * remove console log * merged with main * chore: charts update dirty files * chore: nx format:write update dirty files * adds CreateDelegationConfirm modal and prefills Create form with values from search params * chore: nx format:write update dirty files * use identity resolver fixes in response to comments * chore: nx format:write update dirty files * created delegation-delegation-type.model.ts and updated findAllScopesTo in delegation-scope.service.ts * fix broken tests * tests for findAllScopesTo * added validTo to delegationDelegationType * set general mandate as type in ids select account prompt * Get general mandate to delegations-to on service-portal * remove duplicate case * small refactor * chore: nx format:write update dirty files * Mask nationalId in url * format national id * fix tests after merge with main * chore: nx format:write update dirty files * fix duplicate referenceId's * fix import * remove console log and unused variables * chore: nx format:write update dirty files * move general mandate tests to new file * add zendesk validation * feat(auth-admin): Delete delegation UI (#16073) * Changes includeArchived from Param to Query Fixes openapi.yaml error * adds Oauth2 to openApi document builder * validates Zendesk ticket when creating delegation * Adds delete button to delegation access card and call delete mutation * chore: nx format:write update dirty files * add referenceId to delegation query --------- Co-authored-by: andes-it * feat(auth-admin): Create paper delegation zendesk integration (#16074) * Changes includeArchived from Param to Query Fixes openapi.yaml error * adds Oauth2 to openApi document builder * validates Zendesk ticket when creating delegation * chore: nx format:write update dirty files --------- Co-authored-by: andes-it * connect changes and modify incoming delegations for new ddt table * fix comments from PR * fix pr comment * chore: nx format:write update dirty files * chore: nx format:write update dirty files * fix pr comment * add tests for create * fix pr comments * simplify var names * chore: nx format:write update dirty files * add index for general mandate * fix pr comments * fix pr comments --------- Co-authored-by: andes-it Co-authored-by: Magnea Rún Vignisdóttir Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../app/v2/clients/me-clients.controller.ts | 3 +- .../delegation-admin.controller.ts | 26 ++ .../test/delegation-admin.auth.spec.ts | 135 +++++++ .../delegations/test/delegation-admin.spec.ts | 332 ++++++++++++++++++ apps/services/auth/admin-api/src/openApi.ts | 19 + .../delegation-admin.resolver.ts | 10 + .../delegation-admin.service.ts | 11 +- .../auth/src/lib/models/delegation.model.ts | 3 + libs/auth-api-lib/sequelize.config.js | 8 +- libs/auth-api-lib/src/index.ts | 2 + .../admin/delegation-admin-custom.service.ts | 275 +++++++++++---- .../src/lib/delegations/constants/zendesk.ts | 5 + .../delegations-incoming-custom.service.ts | 9 +- .../delegations/delegations-index.service.ts | 19 + .../src/lib/delegations/delegations.module.ts | 6 + .../dto/create-paper-delegation.dto.ts | 21 ++ .../delegation-delegation-type.model.ts | 2 +- .../delegations/models/delegation.model.ts | 14 +- .../src/lib/environments/environment.ts | 9 + .../src/lib/environments/index.ts | 1 + .../zendesk/src/lib/zendesk.service.ts | 39 +- .../src/components/DelegationList.tsx | 38 +- .../DelegationAdmin.graphql | 6 + .../DelegationAdmin.tsx | 12 +- .../src/components/access/AccessCard.tsx | 24 +- .../incoming/DelegationsIncoming.tsx | 2 + .../outgoing/DelegationsOutgoing.tsx | 2 + .../testing/src/fixtures/fixture-factory.ts | 2 + .../auth/testing/src/fixtures/types.ts | 7 +- 29 files changed, 925 insertions(+), 117 deletions(-) create mode 100644 apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts create mode 100644 apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts create mode 100644 libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts create mode 100644 libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts create mode 100644 libs/auth-api-lib/src/lib/environments/environment.ts create mode 100644 libs/auth-api-lib/src/lib/environments/index.ts diff --git a/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts b/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts index 5bb808ac4fd0..649e2ba61991 100644 --- a/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts +++ b/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts @@ -7,6 +7,7 @@ import { Post, UseGuards, Delete, + Query, } from '@nestjs/common' import { ApiSecurity, ApiTags } from '@nestjs/swagger' @@ -74,7 +75,7 @@ export class MeClientsController { @CurrentUser() user: User, @Param('tenantId') tenantId: string, @Param('clientId') clientId: string, - @Param('includeArchived') includeArchived?: boolean, + @Query('includeArchived') includeArchived?: boolean, ): Promise { return this.clientsService.findByTenantIdAndClientId( tenantId, diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts b/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts index 1889f4886baf..4ab51563c17a 100644 --- a/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts +++ b/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts @@ -1,9 +1,11 @@ import { + Body, Controller, Delete, Get, Headers, Param, + Post, UseGuards, } from '@nestjs/common' import { ApiTags } from '@nestjs/swagger' @@ -16,8 +18,10 @@ import { User, } from '@island.is/auth-nest-tools' import { + CreatePaperDelegationDto, DelegationAdminCustomDto, DelegationAdminCustomService, + DelegationDTO, } from '@island.is/auth-api-lib' import { Documentation } from '@island.is/nest/swagger' import { Audit, AuditService } from '@island.is/nest/audit' @@ -65,6 +69,28 @@ export class DelegationAdminController { ) } + @Post() + @Scopes(DelegationAdminScopes.admin) + @Documentation({ + response: { status: 201, type: DelegationDTO }, + }) + create( + @CurrentUser() user: User, + @Body() delegation: CreatePaperDelegationDto, + ): Promise { + return this.auditService.auditPromise( + { + auth: user, + namespace, + action: 'create', + resources: (result) => { + return result?.id ?? undefined + }, + }, + this.delegationAdminService.createDelegation(user, delegation), + ) + } + @Delete(':delegationId') @Scopes(DelegationAdminScopes.admin) @Documentation({ diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts new file mode 100644 index 000000000000..f97758aaf310 --- /dev/null +++ b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts @@ -0,0 +1,135 @@ +import request from 'supertest' + +import { + getRequestMethod, + setupApp, + setupAppWithoutAuth, + TestApp, + TestEndpointOptions, +} from '@island.is/testing/nest' +import { User } from '@island.is/auth-nest-tools' +import { FixtureFactory } from '@island.is/services/auth/testing' +import { createCurrentUser } from '@island.is/testing/fixtures' +import { DelegationAdminScopes } from '@island.is/auth/scopes' +import { SequelizeConfigService } from '@island.is/auth-api-lib' + +import { AppModule } from '../../../app.module' + +describe('withoutAuth and permissions', () => { + async function formatUrl(app: TestApp, endpoint: string, user?: User) { + if (!endpoint.includes(':delegation')) { + return endpoint + } + const factory = new FixtureFactory(app) + const domain = await factory.createDomain({ + name: 'd1', + apiScopes: [{ name: 's1' }], + }) + const delegation = await factory.createCustomDelegation({ + fromNationalId: user?.nationalId, + domainName: domain.name, + scopes: [{ scopeName: 's1' }], + }) + return endpoint.replace(':delegation', encodeURIComponent(delegation.id)) + } + + it.each` + method | endpoint + ${'GET'} | ${'/delegation-admin'} + ${'DELETE'} | ${'/delegation-admin/:delegation'} + `( + '$method $endpoint should return 401 when user is not authenticated', + async ({ method, endpoint }) => { + // Arrange + const app = await setupAppWithoutAuth({ + AppModule, + SequelizeConfigService, + dbType: 'postgres', + }) + const server = request(app.getHttpServer()) + const url = await formatUrl(app, endpoint) + + // Act + const res = await getRequestMethod(server, method)(url) + + // Assert + expect(res.status).toEqual(401) + expect(res.body).toMatchObject({ + status: 401, + type: 'https://httpstatuses.org/401', + title: 'Unauthorized', + }) + }, + ) + + it.each` + method | endpoint + ${'GET'} | ${'/delegation-admin'} + ${'DELETE'} | ${'/delegation-admin/:delegation'} + `( + '$method $endpoint should return 403 Forbidden when user does not have the correct scope', + async ({ method, endpoint }: TestEndpointOptions) => { + // Arrange + const user = createCurrentUser() + const app = await setupApp({ + AppModule, + SequelizeConfigService, + user, + dbType: 'postgres', + }) + const server = request(app.getHttpServer()) + const url = await formatUrl(app, endpoint, user) + + // Act + const res = await getRequestMethod(server, method)(url) + + // Assert + expect(res.status).toEqual(403) + expect(res.body).toMatchObject({ + status: 403, + type: 'https://httpstatuses.org/403', + title: 'Forbidden', + detail: 'Forbidden resource', + }) + + // CleanUp + app.cleanUp() + }, + ) + + it.each` + method | endpoint + ${'DELETE'} | ${'/delegation-admin/:delegation'} + `( + '$method $endpoint should return 403 Forbidden when user does not have the admin scope', + async ({ method, endpoint }: TestEndpointOptions) => { + // Arrange + const user = createCurrentUser({ + scope: [DelegationAdminScopes.read], + }) + const app = await setupApp({ + AppModule, + SequelizeConfigService, + user, + dbType: 'postgres', + }) + const server = request(app.getHttpServer()) + const url = await formatUrl(app, endpoint, user) + + // Act + const res = await getRequestMethod(server, method)(url) + + // Assert + expect(res.status).toEqual(403) + expect(res.body).toMatchObject({ + status: 403, + type: 'https://httpstatuses.org/403', + title: 'Forbidden', + detail: 'Forbidden resource', + }) + + // CleanUp + app.cleanUp() + }, + ) +}) diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts new file mode 100644 index 000000000000..718b6427b800 --- /dev/null +++ b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts @@ -0,0 +1,332 @@ +import request from 'supertest' + +import { getRequestMethod, setupApp, TestApp } from '@island.is/testing/nest' +import { User } from '@island.is/auth-nest-tools' +import { FixtureFactory } from '@island.is/services/auth/testing' +import { + createCurrentUser, + createNationalRegistryUser, +} from '@island.is/testing/fixtures' +import { DelegationAdminScopes } from '@island.is/auth/scopes' +import addDays from 'date-fns/addDays' +import { + CreatePaperDelegationDto, + Delegation, + DELEGATION_TAG, + DelegationDelegationType, + DelegationsIndexService, + SequelizeConfigService, + ZENDESK_CUSTOM_FIELDS, +} from '@island.is/auth-api-lib' + +import { AppModule } from '../../../app.module' +import { AuthDelegationType } from '@island.is/shared/types' +import { getModelToken } from '@nestjs/sequelize' +import { faker } from '@island.is/shared/mocking' +import { TicketStatus, ZendeskService } from '@island.is/clients/zendesk' +import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' + +const currentUser = createCurrentUser({ + scope: [DelegationAdminScopes.read, DelegationAdminScopes.admin], +}) + +describe('DelegationAdmin - With authentication', () => { + let app: TestApp + let server: request.SuperTest + let factory: FixtureFactory + let zendeskService: ZendeskService + let nationalRegistryApi: NationalRegistryClientService + let delegationIndexServiceApi: DelegationsIndexService + + beforeEach(async () => { + app = await setupApp({ + AppModule, + SequelizeConfigService, + user: currentUser, + dbType: 'postgres', + }) + + server = request(app.getHttpServer()) + factory = new FixtureFactory(app) + + zendeskService = app.get(ZendeskService) + nationalRegistryApi = app.get(NationalRegistryClientService) + delegationIndexServiceApi = app.get(DelegationsIndexService) + + jest + .spyOn(delegationIndexServiceApi, 'indexCustomDelegations') + .mockImplementation(async () => { + return + }) + + jest + .spyOn(delegationIndexServiceApi, 'indexGeneralMandateDelegations') + .mockImplementation(async () => { + return + }) + }) + + afterEach(async () => { + await app.cleanUp() + }) + + async function createDelegationAdmin(user?: User) { + const domain = await factory.createDomain({ + name: 'd1', + apiScopes: [ + { name: 's1', supportedDelegationTypes: [AuthDelegationType.Custom] }, + ], + }) + + return factory.createCustomDelegation({ + fromNationalId: user?.nationalId ?? '', + domainName: domain.name, + scopes: [{ scopeName: 's1' }], + referenceId: 'ref1', + }) + } + + describe('GET /delegation-admin', () => { + it('GET /delegation-admin should return delegations for nationalId', async () => { + // Arrange + const delegation = await createDelegationAdmin(currentUser) + // Act + const res = await getRequestMethod( + server, + 'GET', + )('/delegation-admin').set('X-Query-National-Id', currentUser.nationalId) + + // Assert + expect(res.status).toEqual(200) + expect(res.body['outgoing'][0].id).toEqual(delegation.id) + }) + }) + + describe('DELETE /delegation-admin/:delegation', () => { + it('DELETE /delegation-admin/:delegation should not delete delegation that has no reference id', async () => { + // Arrange + const delegationModel = await app.get(getModelToken(Delegation)) + const delegation = await createDelegationAdmin(currentUser) + // Remove the referenceId + await delegationModel.update( + { + referenceId: null, + }, + { + where: { + id: delegation.id, + }, + }, + ) + + // Act + const res = await getRequestMethod( + server, + 'DELETE', + )(`/delegation-admin/${delegation.id}`) + + // Assert + expect(res.status).toEqual(204) + + // Assert db + const deletedDelegation = await delegationModel.findByPk(delegation.id) + + expect(deletedDelegation).not.toBeNull() + }) + + it('DELETE /delegation-admin/:delegation should delete delegation', async () => { + // Arrange + const delegation = await createDelegationAdmin(currentUser) + + // Act + const res = await getRequestMethod( + server, + 'DELETE', + )(`/delegation-admin/${delegation.id}`) + + // Assert + expect(res.status).toEqual(204) + + // Assert db + const delegationModel = await app.get(getModelToken(Delegation)) + const deletedDelegation = await delegationModel.findByPk(delegation.id) + + expect(deletedDelegation).toBeNull() + }) + + it('DELETE /delegation-admin/:delegation should throw error since id does not exist', async () => { + // Arrange + await createDelegationAdmin(currentUser) + + const invalidId = faker.datatype.uuid() + // Act + const res = await getRequestMethod( + server, + 'DELETE', + )(`/delegation-admin/${invalidId}`) + + // Assert + expect(res.status).toEqual(204) + + // Assert db + const delegationModel = await app.get(getModelToken(Delegation)) + const deletedDelegation = await delegationModel.findAll() + + expect(deletedDelegation).not.toBeNull() + }) + }) + + describe('POST /delegation-admin', () => { + const toNationalId = '0101302399' + const fromNationalId = '0101307789' + + let zendeskServiceApiSpy: jest.SpyInstance + let nationalRegistryApiSpy: jest.SpyInstance + + let delegationModel: typeof Delegation + let delegationDelegationTypeModel: typeof DelegationDelegationType + + beforeEach(async () => { + delegationModel = await app.get(getModelToken(Delegation)) + delegationDelegationTypeModel = await app.get( + getModelToken(DelegationDelegationType), + ) + + await factory.createDomain({ + name: 'd1', + apiScopes: [ + { + name: 's1', + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.GeneralMandate, + ], + }, + ], + }) + + mockZendeskService(toNationalId, fromNationalId) + mockNationalRegistryService() + }) + + const mockNationalRegistryService = () => { + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + const user = createNationalRegistryUser({ + nationalId: id, + }) + + return user ?? null + }) + } + + const mockZendeskService = ( + toNationalId: string, + fromNationalId: string, + ) => { + zendeskServiceApiSpy = jest + .spyOn(zendeskService, 'getTicket') + .mockImplementation((ticketId: string) => { + return new Promise((resolve) => + resolve({ + id: ticketId, + tags: [DELEGATION_TAG], + status: TicketStatus.Solved, + custom_fields: [ + { + id: ZENDESK_CUSTOM_FIELDS.DelegationToReferenceId, + value: toNationalId, + }, + { + id: ZENDESK_CUSTOM_FIELDS.DelegationFromReferenceId, + value: fromNationalId, + }, + ], + }), + ) + }) + } + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('POST /delegation-admin should create delegation', async () => { + // Arrange + const delegation: CreatePaperDelegationDto = { + toNationalId, + fromNationalId, + referenceId: 'ref1', + validTo: addDays(new Date(), 3), + } + + // Act + const res = await getRequestMethod( + server, + 'POST', + )('/delegation-admin').send(delegation) + + // Assert + expect(res.status).toEqual(201) + expect(res.body).toHaveProperty('id') + expect(res.body.fromNationalId).toEqual(fromNationalId) + expect(res.body.toNationalId).toEqual(toNationalId) + expect(res.body.referenceId).toEqual(delegation.referenceId) + expect(res.body.validTo).toEqual(delegation.validTo?.toISOString()) + }) + + it('POST /delegation-admin should create delegation with no expiration date', async () => { + // Arrange + const delegation: CreatePaperDelegationDto = { + toNationalId, + fromNationalId, + referenceId: 'ref1', + } + + // Act + const res = await getRequestMethod( + server, + 'POST', + )('/delegation-admin').send(delegation) + + // Assert + expect(res.status).toEqual(201) + expect(res.body).toHaveProperty('id') + expect(res.body.fromNationalId).toEqual(fromNationalId) + expect(res.body.toNationalId).toEqual(toNationalId) + expect(res.body.referenceId).toEqual(delegation.referenceId) + expect(res.body).not.toHaveProperty('validTo') + + // Assert db + const createdDelegation = await delegationModel.findByPk(res.body.id) + const createdDelegationDelegationType = + await delegationDelegationTypeModel.findOne({ + where: { + delegationId: res.body.id, + }, + }) + + expect(createdDelegation).not.toBeNull() + expect(createdDelegationDelegationType).not.toBeNull() + }) + + it('POST /delegation-admin should not create delegation with company national id', async () => { + // Arrange + const delegation: CreatePaperDelegationDto = { + toNationalId: '5005005001', + fromNationalId, + referenceId: 'ref1', + } + + // Act + const res = await getRequestMethod( + server, + 'POST', + )('/delegation-admin').send(delegation) + + // Assert + expect(res.status).toEqual(400) + }) + }) +}) diff --git a/apps/services/auth/admin-api/src/openApi.ts b/apps/services/auth/admin-api/src/openApi.ts index 7e1b27bf3538..64ae673f7545 100644 --- a/apps/services/auth/admin-api/src/openApi.ts +++ b/apps/services/auth/admin-api/src/openApi.ts @@ -1,8 +1,27 @@ import { DocumentBuilder } from '@nestjs/swagger' +import { environment } from './environments' +import { AuthScope } from '@island.is/auth/scopes' export const openApi = new DocumentBuilder() .setTitle('IdentityServer Admin api') .setDescription('Api for administration.') .setVersion('2.0') .addTag('auth-admin-api') + .addOAuth2( + { + type: 'oauth2', + description: + 'Authentication and authorization using island.is authentication service (IAS).', + flows: { + authorizationCode: { + authorizationUrl: `${environment.auth.issuer}/connect/authorize`, + tokenUrl: `${environment.auth.issuer}/connect/token`, + scopes: { + openid: 'Default openid scope', + }, + }, + }, + }, + 'ias', + ) .build() diff --git a/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts index 37f89bac5db3..b871d2c40668 100644 --- a/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts +++ b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts @@ -109,6 +109,16 @@ export class DelegationAdminResolver { @Loader(DomainLoader) domainLoader: DomainDataLoader, @Parent() delegation: DelegationDTO, ): Promise { + if (!delegation.domainName) { + return { + name: '', + displayName: '', + description: '', + nationalId: '', + organisationLogoKey: '', + } + } + const domainName = delegation.domainName ?? ISLAND_DOMAIN const domain = await domainLoader.load({ lang: 'is', diff --git a/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts index e6ce773980a3..00b3ee818f2b 100644 --- a/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts +++ b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts @@ -42,15 +42,8 @@ export class DelegationAdminService { } async createDelegationAdmin(user: User, input: CreateDelegationInput) { - // Mock response - return Promise.resolve({ - id: 'some-id', - fromNationalId: '0101302399', - toNationalId: '0101302399', - scopes: [], - validTo: null, - type: 'Custom', - provider: 'delegationdb', + return this.delegationsWithAuth(user).delegationAdminControllerCreate({ + createPaperDelegationDto: input, }) } } diff --git a/libs/api/domains/auth/src/lib/models/delegation.model.ts b/libs/api/domains/auth/src/lib/models/delegation.model.ts index af3cf9cc5cd4..91b22ada9926 100644 --- a/libs/api/domains/auth/src/lib/models/delegation.model.ts +++ b/libs/api/domains/auth/src/lib/models/delegation.model.ts @@ -56,6 +56,9 @@ export abstract class Delegation { @Field(() => AuthDelegationProvider) provider!: AuthDelegationProvider + + @Field(() => String, { nullable: true }) + referenceId?: string } @ObjectType('AuthLegalGuardianDelegation', { diff --git a/libs/auth-api-lib/sequelize.config.js b/libs/auth-api-lib/sequelize.config.js index 1808cea88c41..de497b35ab6a 100644 --- a/libs/auth-api-lib/sequelize.config.js +++ b/libs/auth-api-lib/sequelize.config.js @@ -1,12 +1,12 @@ /* eslint-env node */ module.exports = { development: { - username: process.env.DB_USER ?? 'dev_db', - password: process.env.DB_PASS ?? 'dev_db', - database: process.env.DB_NAME ?? 'dev_db', + username: process.env.DB_USER_AUTH_DB ?? 'dev_db', + password: process.env.DB_PASS_AUTH_DB ?? 'dev_db', + database: process.env.DB_USER_AUTH_DB ?? 'dev_db', host: 'localhost', dialect: 'postgres', - port: process.env.DB_PORT ?? 5433, + port: process.env.DB_PORT_AUTH_DB ?? 5433, seederStorage: 'sequelize', }, test: { diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts index 68842edcc489..b9305d456101 100644 --- a/libs/auth-api-lib/src/index.ts +++ b/libs/auth-api-lib/src/index.ts @@ -39,6 +39,7 @@ export * from './lib/delegations/types/delegationDirection' export * from './lib/delegations/types/delegationType' export * from './lib/delegations/types/delegationRecord' export * from './lib/delegations/types/delegationValidity' +export * from './lib/delegations/dto/create-paper-delegation.dto' export * from './lib/delegations/dto/delegation-scope.dto' export * from './lib/delegations/dto/delegation-admin-custom.dto' export * from './lib/delegations/dto/delegation.dto' @@ -58,6 +59,7 @@ export * from './lib/delegations/DelegationConfig' export * from './lib/delegations/utils/scopes' export * from './lib/delegations/admin/delegation-admin-custom.service' export * from './lib/delegations/constants/names' +export * from './lib/delegations/constants/zendesk' // Resources module export * from './lib/resources/resources.module' diff --git a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts index f5f878a88e15..43ced573b3d8 100644 --- a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts @@ -1,101 +1,211 @@ -import { Injectable } from '@nestjs/common' +import { BadRequestException, Injectable } from '@nestjs/common' import { InjectModel } from '@nestjs/sequelize' +import { Sequelize } from 'sequelize-typescript' +import kennitala from 'kennitala' +import { uuid } from 'uuidv4' + +import { AuthDelegationType } from '@island.is/shared/types' +import { User } from '@island.is/auth-nest-tools' +import { NoContentException } from '@island.is/nest/problem' +import { + Ticket, + TicketStatus, + ZendeskService, +} from '@island.is/clients/zendesk' import { Delegation } from '../models/delegation.model' import { DelegationAdminCustomDto } from '../dto/delegation-admin-custom.dto' import { DelegationScope } from '../models/delegation-scope.model' import { ApiScope } from '../../resources/models/api-scope.model' import { ApiScopeDelegationType } from '../../resources/models/api-scope-delegation-type.model' -import { AuthDelegationType } from '@island.is/shared/types' -import { User } from '@island.is/auth-nest-tools' import { DelegationResourcesService } from '../../resources/delegation-resources.service' import { DelegationsIndexService } from '../delegations-index.service' import { DelegationScopeService } from '../delegation-scope.service' -import { NoContentException } from '@island.is/nest/problem' -import { Sequelize } from 'sequelize-typescript' +import { CreatePaperDelegationDto } from '../dto/create-paper-delegation.dto' +import { DelegationDTO } from '../dto/delegation.dto' +import { NamesService } from '../names.service' +import { DELEGATION_TAG, ZENDESK_CUSTOM_FIELDS } from '../constants/zendesk' +import { DelegationDelegationType } from '../models/delegation-delegation-type.model' +import { DelegationsIncomingCustomService } from '../delegations-incoming-custom.service' +import { DelegationValidity } from '../types/delegationValidity' @Injectable() export class DelegationAdminCustomService { constructor( @InjectModel(Delegation) private delegationModel: typeof Delegation, + @InjectModel(DelegationDelegationType) + private delegationDelegationTypeModel: typeof DelegationDelegationType, + private readonly zendeskService: ZendeskService, private delegationResourceService: DelegationResourcesService, + private delegationsIncomingCustomService: DelegationsIncomingCustomService, private delegationIndexService: DelegationsIndexService, private delegationScopeService: DelegationScopeService, + private namesService: NamesService, private sequelize: Sequelize, ) {} + private getNationalIdsFromZendeskTicket(ticket: Ticket): { + fromReferenceId: string + toReferenceId: string + } { + const fromReferenceId = ticket.custom_fields.find( + (field) => field.id === ZENDESK_CUSTOM_FIELDS.DelegationFromReferenceId, + ) + const toReferenceId = ticket.custom_fields.find( + (field) => field.id === ZENDESK_CUSTOM_FIELDS.DelegationToReferenceId, + ) + + if (!fromReferenceId || !toReferenceId) { + throw new BadRequestException( + 'Zendesk ticket is missing required custom fields', + ) + } + + return { + fromReferenceId: fromReferenceId.value, + toReferenceId: toReferenceId.value, + } + } + async getAllDelegationsByNationalId( nationalId: string, ): Promise { - const incomingDelegations = await this.delegationModel.findAll({ - where: { - toNationalId: nationalId, - }, - include: [ - { - model: DelegationScope, - required: true, - include: [ - { - model: ApiScope, - as: 'apiScope', - required: true, - where: { - enabled: true, - }, - include: [ - { - model: ApiScopeDelegationType, - required: true, - where: { - delegationType: AuthDelegationType.Custom, - }, - }, - ], - }, - ], + const [ + incomingCustomDelegations, + incomingGeneralDelegations, + outgoingCustomDelegations, + outgoingGeneralDelegations, + ] = await Promise.all([ + this.delegationsIncomingCustomService.findAllValidIncoming({ + nationalId: nationalId, + validity: DelegationValidity.ALL, + }), + this.delegationsIncomingCustomService.findAllValidGeneralMandate({ + nationalId: nationalId, + }), + this.delegationModel.findAll({ + where: { + fromNationalId: nationalId, }, - ], - }) - - const outgoingDelegations = await this.delegationModel.findAll({ - where: { - fromNationalId: nationalId, - }, - include: [ - { - model: DelegationScope, - required: true, - include: [ - { - model: ApiScope, - required: true, - as: 'apiScope', - where: { - enabled: true, - }, - include: [ - { - model: ApiScopeDelegationType, - required: true, - where: { - delegationType: AuthDelegationType.Custom, - }, + include: [ + { + model: DelegationScope, + required: true, + include: [ + { + model: ApiScope, + required: true, + as: 'apiScope', + where: { + enabled: true, }, - ], - }, - ], + include: [ + { + model: ApiScopeDelegationType, + required: true, + where: { + delegationType: AuthDelegationType.Custom, + }, + }, + ], + }, + ], + }, + ], + }), + this.delegationModel.findAll({ + where: { + fromNationalId: nationalId, }, - ], - }) + include: [ + { + model: DelegationDelegationType, + required: true, + where: { + delegationTypeId: AuthDelegationType.GeneralMandate, + }, + }, + ], + }), + ]) return { - incoming: incomingDelegations.map((delegation) => delegation.toDTO()), - outgoing: outgoingDelegations.map((delegation) => delegation.toDTO()), + incoming: [...incomingCustomDelegations, ...incomingGeneralDelegations], + outgoing: [ + ...outgoingGeneralDelegations.map((d) => + d.toDTO(AuthDelegationType.GeneralMandate), + ), + ...outgoingCustomDelegations.map((delegation) => delegation.toDTO()), + ], } } + async createDelegation( + user: User, + delegation: CreatePaperDelegationDto, + ): Promise { + this.validatePersonsNationalIds( + delegation.toNationalId, + delegation.fromNationalId, + ) + + const zendeskCase = await this.zendeskService.getTicket( + delegation.referenceId, + ) + + if (!zendeskCase.tags.includes(DELEGATION_TAG)) { + throw new BadRequestException('Zendesk ticket is missing required tag') + } + + if (zendeskCase.status !== TicketStatus.Solved) { + throw new BadRequestException('Zendesk case is not solved') + } + + const { fromReferenceId, toReferenceId } = + this.getNationalIdsFromZendeskTicket(zendeskCase) + + if ( + fromReferenceId !== delegation.fromNationalId || + toReferenceId !== delegation.toNationalId + ) { + throw new BadRequestException( + 'Zendesk ticket nationalIds does not match delegation nationalIds', + ) + } + + const [fromDisplayName, toName] = await Promise.all([ + this.namesService.getPersonName(delegation.fromNationalId), + this.namesService.getPersonName(delegation.toNationalId), + ]) + + const newDelegation = await this.delegationModel.create( + { + id: uuid(), + toNationalId: delegation.toNationalId, + fromNationalId: delegation.fromNationalId, + createdByNationalId: user.actor?.nationalId ?? user.nationalId, + referenceId: delegation.referenceId, + toName, + fromDisplayName, + delegationDelegationTypes: [ + { + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: delegation.validTo, + }, + ] as DelegationDelegationType[], + }, + { + include: [this.delegationDelegationTypeModel], + }, + ) + + // Index delegations for the toNationalId + void this.indexDelegations(delegation.toNationalId) + + return newDelegation.toDTO(AuthDelegationType.GeneralMandate) + } + async deleteDelegation(user: User, delegationId: string): Promise { const delegation = await this.delegationModel.findByPk(delegationId) @@ -103,6 +213,10 @@ export class DelegationAdminCustomService { throw new NoContentException() } + if (!delegation.referenceId) { + throw new NoContentException() + } + const userScopes = await this.delegationResourceService.findScopes( user, delegation.domainName ?? null, @@ -119,6 +233,7 @@ export class DelegationAdminCustomService { const remainingScopes = await this.delegationScopeService.findAll( delegationId, ) + if (remainingScopes.length === 0) { await this.delegationModel.destroy({ transaction, @@ -128,10 +243,32 @@ export class DelegationAdminCustomService { }) } - // Index custom delegations for the toNationalId - void this.delegationIndexService.indexCustomDelegations( - delegation.toNationalId, - ) + // Index delegations for the toNationalId + void this.indexDelegations(delegation.toNationalId) }) } + + private validatePersonsNationalIds( + toNationalId: string, + fromNationalId: string, + ) { + if (toNationalId === fromNationalId) { + throw new BadRequestException( + 'Cannot create a delegation between the same nationalId.', + ) + } + + if ( + !(kennitala.isPerson(fromNationalId) && kennitala.isPerson(toNationalId)) + ) { + throw new BadRequestException( + 'National ids needs to be valid person national ids', + ) + } + } + + private indexDelegations(nationalId: string) { + void this.delegationIndexService.indexCustomDelegations(nationalId) + void this.delegationIndexService.indexGeneralMandateDelegations(nationalId) + } } diff --git a/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts b/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts new file mode 100644 index 000000000000..b6e4ba2f6cd6 --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts @@ -0,0 +1,5 @@ +export const DELEGATION_TAG = 'umsokn_um_umboð_a_mínum_síðum' +export const ZENDESK_CUSTOM_FIELDS = { + DelegationFromReferenceId: 21401464004498, + DelegationToReferenceId: 21401435545234, +} diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts index b13ce486becc..4e07372ce897 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts @@ -33,6 +33,7 @@ import { DelegationDelegationType } from './models/delegation-delegation-type.mo type FindAllValidIncomingOptions = { nationalId: string domainName?: string + validity?: DelegationValidity } type FromNameInfo = { @@ -59,13 +60,17 @@ export class DelegationsIncomingCustomService { ) {} async findAllValidIncoming( - { nationalId, domainName }: FindAllValidIncomingOptions, + { + nationalId, + domainName, + validity = DelegationValidity.NOW, + }: FindAllValidIncomingOptions, useMaster = false, ): Promise { const { delegations, fromNameInfo } = await this.findAllIncoming( { nationalId, - validity: DelegationValidity.NOW, + validity, domainName, }, useMaster, diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts index f6af5c4b46a5..46b51cd42f48 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts @@ -280,6 +280,12 @@ export class DelegationsIndexService { await this.saveToIndex(nationalId, delegations) } + /* Index incoming general mandate delegations */ + async indexGeneralMandateDelegations(nationalId: string) { + const delegations = await this.getGeneralMandateDelegation(nationalId, true) + await this.saveToIndex(nationalId, delegations) + } + /* Index incoming personal representative delegations */ async indexRepresentativeDelegations(nationalId: string) { const delegations = await this.getRepresentativeDelegations( @@ -483,6 +489,19 @@ export class DelegationsIndexService { ) } + private async getGeneralMandateDelegation( + nationalId: string, + useMaster = false, + ) { + const delegation = + await this.delegationsIncomingCustomService.findAllValidGeneralMandate( + { nationalId }, + useMaster, + ) + + return delegation.map(toDelegationIndexInfo) + } + private async getCustomDelegations(nationalId: string, useMaster = false) { const delegations = await this.delegationsIncomingCustomService.findAllValidIncoming( diff --git a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts index e5f79d1aab6c..1fb0530b43d0 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts @@ -6,6 +6,10 @@ import { NationalRegistryClientModule } from '@island.is/clients/national-regist import { CompanyRegistryClientModule } from '@island.is/clients/rsk/company-registry' import { SyslumennClientModule } from '@island.is/clients/syslumenn' import { FeatureFlagModule } from '@island.is/nest/feature-flags' +import { + ZendeskModule, + ZendeskServiceOptions, +} from '@island.is/clients/zendesk' import { ClientAllowedScope } from '../clients/models/client-allowed-scope.model' import { Client } from '../clients/models/client.model' @@ -28,6 +32,7 @@ import { DelegationsIncomingService } from './delegations-incoming.service' import { DelegationsIndexService } from './delegations-index.service' import { DelegationsOutgoingService } from './delegations-outgoing.service' import { DelegationsService } from './delegations.service' +import { environment } from '../environments' import { DelegationDelegationType } from './models/delegation-delegation-type.model' import { DelegationIndexMeta } from './models/delegation-index-meta.model' import { DelegationIndex } from './models/delegation-index.model' @@ -46,6 +51,7 @@ import { NamesService } from './names.service' CompanyRegistryClientModule, UserIdentitiesModule, FeatureFlagModule, + ZendeskModule.register(environment.zendeskOptions as ZendeskServiceOptions), SequelizeModule.forFeature([ ApiScope, ApiScopeDelegationType, diff --git a/libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts b/libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts new file mode 100644 index 000000000000..addcc91b66b5 --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts @@ -0,0 +1,21 @@ +import { IsDateString, IsOptional, IsString } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class CreatePaperDelegationDto { + @IsString() + @ApiProperty() + fromNationalId!: string + + @IsString() + @ApiProperty() + toNationalId!: string + + @ApiProperty() + @IsString() + referenceId!: string + + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ nullable: true, type: Date }) + validTo?: Date | null +} diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts index ba00c7fc4b3f..f84933c7a80a 100644 --- a/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts @@ -51,7 +51,7 @@ export class DelegationDelegationType extends Model< type: DataType.DATE, allowNull: true, }) - validTo?: Date + validTo?: Date | null @CreatedAt readonly created!: CreationOptional 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 41ef58402d08..cff697005338 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 @@ -25,6 +25,7 @@ import { AuthDelegationType, } from '@island.is/shared/types' import { DelegationDelegationType } from './delegation-delegation-type.model' +import { isDefined } from '@island.is/shared/utils' @Table({ tableName: 'delegation', @@ -96,6 +97,16 @@ export class Delegation extends Model< referenceId?: string get validTo(): Date | null | undefined { + if ( + this.delegationDelegationTypes && + this.delegationDelegationTypes.length > 0 + ) { + const dates = this.delegationDelegationTypes + .map((x) => x.validTo) + .filter((x) => isDefined(x)) as Array + return max(dates) + } + // 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) if (withNullValue) { @@ -104,7 +115,7 @@ export class Delegation extends Model< // 2. Find items with value in the array const dates = (this.delegationScopes - ?.filter((x) => x.validTo !== null && x.validTo !== undefined) + ?.filter((x) => isDefined(x.validTo)) .map((x) => x.validTo) || []) as Array // Return the max value @@ -136,6 +147,7 @@ export class Delegation extends Model< : [], provider: AuthDelegationProvider.Custom, type: type, + referenceId: this.referenceId, domainName: this.domainName, } } diff --git a/libs/auth-api-lib/src/lib/environments/environment.ts b/libs/auth-api-lib/src/lib/environments/environment.ts new file mode 100644 index 000000000000..9db41f8ead3b --- /dev/null +++ b/libs/auth-api-lib/src/lib/environments/environment.ts @@ -0,0 +1,9 @@ +const config = { + zendeskOptions: { + email: process.env.ZENDESK_CONTACT_FORM_EMAIL, + token: process.env.ZENDESK_CONTACT_FORM_TOKEN, + subdomain: process.env.ZENDESK_CONTACT_FORM_SUBDOMAIN, + }, +} + +export default config diff --git a/libs/auth-api-lib/src/lib/environments/index.ts b/libs/auth-api-lib/src/lib/environments/index.ts new file mode 100644 index 000000000000..f1c9690a5bd4 --- /dev/null +++ b/libs/auth-api-lib/src/lib/environments/index.ts @@ -0,0 +1 @@ +export { default as environment } from './environment' diff --git a/libs/clients/zendesk/src/lib/zendesk.service.ts b/libs/clients/zendesk/src/lib/zendesk.service.ts index 13739a9c89ce..3927ee2ea8cf 100644 --- a/libs/clients/zendesk/src/lib/zendesk.service.ts +++ b/libs/clients/zendesk/src/lib/zendesk.service.ts @@ -5,7 +5,16 @@ import { LOGGER_PROVIDER } from '@island.is/logging' export const ZENDESK_OPTIONS = 'ZENDESK_OPTIONS' -export type Ticket = { +export enum TicketStatus { + Open = 'open', + Pending = 'pending', + Solved = 'solved', + Closed = 'closed', + New = 'new', + OnHold = 'on-hold', +} + +export type SubmitTicketInput = { subject?: string message: string requesterId?: number @@ -18,6 +27,13 @@ export type User = { id: number } +export type Ticket = { + id: string + status: TicketStatus | string + custom_fields: Array<{ id: number; value: string }> + tags: Array +} + export interface ZendeskServiceOptions { email: string token: string @@ -121,7 +137,7 @@ export class ZendeskService { subject, requesterId, tags = [], - }: Ticket): Promise { + }: SubmitTicketInput): Promise { const newTicket = JSON.stringify({ ticket: { requester_id: requesterId, @@ -146,4 +162,23 @@ export class ZendeskService { return true } + + async getTicket(ticketId: string): Promise { + try { + const response = await axios.get(`${this.api}/tickets/${ticketId}.json`, { + ...this.params, + }) + + return response.data.ticket + } catch (e) { + const errMsg = 'Failed to get Zendesk ticket' + const description = e.response.data.description + + this.logger.error(errMsg, { + message: description, + }) + + throw new Error(`${errMsg}: ${description}`) + } + } } diff --git a/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx b/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx index b248b3e20505..b8e47dc9168d 100644 --- a/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx +++ b/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx @@ -1,6 +1,8 @@ import { AuthCustomDelegation } from '@island.is/api/schema' import { Box, Stack } from '@island.is/island-ui/core' import { AccessCard } from '@island.is/portals/shared-modules/delegations' +import { useDeleteCustomDelegationAdminMutation } from '../screens/DelegationAdminDetails/DelegationAdmin.generated' +import { useRevalidator } from 'react-router-dom' interface DelegationProps { direction: 'incoming' | 'outgoing' @@ -8,20 +10,34 @@ interface DelegationProps { } const DelegationList = ({ delegationsList, direction }: DelegationProps) => { + const [deleteCustomDelegationAdminMutation] = + useDeleteCustomDelegationAdminMutation() + const { revalidate } = useRevalidator() + return ( - {delegationsList.map((delegation) => ( - { - console.warn('Delete delegation') - }} - /> - ))} + {delegationsList.map((delegation) => { + return ( + { + const { data } = await deleteCustomDelegationAdminMutation({ + variables: { + id: delegation.id as string, + }, + }) + if (data) { + revalidate() + } + }} + /> + ) + })} ) diff --git a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.graphql b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.graphql index ba61ef19ccae..3b5dab5125d8 100644 --- a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.graphql +++ b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.graphql @@ -5,6 +5,7 @@ query getCustomDelegationsAdmin($nationalId: String!) { incoming { id validTo + referenceId domain { name organisationLogoKey @@ -31,6 +32,7 @@ query getCustomDelegationsAdmin($nationalId: String!) { outgoing { id validTo + referenceId domain { name organisationLogoKey @@ -56,3 +58,7 @@ query getCustomDelegationsAdmin($nationalId: String!) { } } } + +mutation deleteCustomDelegationAdmin($id: String!) { + authDeleteAdminDelegation(id: $id) +} diff --git a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx index 4dd86fb617e4..daea025576c2 100644 --- a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx +++ b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx @@ -72,11 +72,11 @@ const DelegationAdminScreen = () => { { label: formatMessage(m.delegationFrom), content: - delegationAdmin.incoming.length > 0 ? ( + delegationAdmin.outgoing.length > 0 ? ( ) : ( @@ -89,11 +89,11 @@ const DelegationAdminScreen = () => { { label: formatMessage(m.delegationTo), content: - delegationAdmin.outgoing.length > 0 ? ( + delegationAdmin.incoming.length > 0 ? ( ) : ( diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx index ab3273fb365d..1537f63de36e 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx @@ -20,7 +20,6 @@ import { m as coreMessages } from '@island.is/portals/core' import uniqBy from 'lodash/uniqBy' import sortBy from 'lodash/sortBy' import { m } from '../../lib/messages' -import { DelegationPaths } from '../../lib/paths' import { AuthApiScope, AuthDelegationType } from '@island.is/api/schema' import { AuthCustomDelegation, @@ -57,6 +56,9 @@ interface AccessCardProps { direction?: 'incoming' | 'outgoing' canModify?: boolean + href?: string + + isAdminView?: boolean } export const AccessCard = ({ @@ -66,6 +68,8 @@ export const AccessCard = ({ variant = 'outgoing', direction = 'outgoing', canModify = true, + href, + isAdminView = false, }: AccessCardProps) => { const { formatMessage } = useLocale() const navigate = useNavigate() @@ -74,10 +78,12 @@ export const AccessCard = ({ const hasTags = tags.length > 0 const isOutgoing = variant === 'outgoing' - const href = `${DelegationPaths.Delegations}/${delegation.id}` const isExpired = useMemo(() => { - if (delegation.validTo) { + if ( + delegation.validTo || + delegation.type === AuthDelegationType.GeneralMandate + ) { return isDateExpired(delegation.validTo) } @@ -176,10 +182,10 @@ export const AccessCard = ({ - {!isOutgoing && ( + {(isAdminView || !isOutgoing) && ( <> {renderDelegationTypeLabel(delegation.type)} - {delegation.domain && ( + {delegation.domain?.name && ( {'|'} @@ -234,7 +240,7 @@ export const AccessCard = ({ {formatMessage(coreMessages.view)} - ) : !isExpired ? ( + ) : !isExpired && href ? ( - ) : ( + ) : href ? ( - )} + ) : null} )} diff --git a/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx b/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx index c444a298339b..526439fcdbb4 100644 --- a/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx +++ b/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx @@ -14,6 +14,7 @@ import { DelegationsEmptyState } from '../DelegationsEmptyState' import { DelegationIncomingModal } from './DelegationIncomingModal/DelegationIncomingModal' import { useAuthDelegationsIncomingQuery } from './DelegationIncomingModal/DelegationIncomingModal.generated' import { AuthCustomDelegationIncoming } from '../../../types/customDelegation' +import { DelegationPaths } from '../../../lib/paths' export const DelegationsIncoming = () => { const { formatMessage, lang = 'is' } = useLocale() @@ -78,6 +79,7 @@ export const DelegationsIncoming = () => { }} direction="incoming" variant="incoming" + href={`${DelegationPaths.Delegations}/${delegation.id}`} /> ), )} diff --git a/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx b/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx index dc7f9174dee8..b4b079342a28 100644 --- a/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx +++ b/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx @@ -17,6 +17,7 @@ import { useAuthDelegationsOutgoingQuery } from './DelegationsOutgoing.generated import { AuthCustomDelegationOutgoing } from '../../../types/customDelegation' import { ALL_DOMAINS } from '../../../constants/domain' import { m } from '../../../lib/messages' +import { DelegationPaths } from '../../../lib/paths' const prepareDomainName = (domainName: string | null) => domainName === ALL_DOMAINS ? null : domainName @@ -114,6 +115,7 @@ export const DelegationsOutgoing = () => { ) }} variant="outgoing" + href={`${DelegationPaths.Delegations}/${delegation.id}`} /> ), )} diff --git a/libs/services/auth/testing/src/fixtures/fixture-factory.ts b/libs/services/auth/testing/src/fixtures/fixture-factory.ts index a8929a9a9f13..b6a37fbf32b5 100644 --- a/libs/services/auth/testing/src/fixtures/fixture-factory.ts +++ b/libs/services/auth/testing/src/fixtures/fixture-factory.ts @@ -376,6 +376,7 @@ export class FixtureFactory { domainName, fromName, scopes = [], + referenceId, }: CreateCustomDelegation): Promise { const delegation = await this.get(Delegation).create({ id: faker.datatype.uuid(), @@ -384,6 +385,7 @@ export class FixtureFactory { domainName, fromDisplayName: fromName ?? faker.name.findName(), toName: faker.name.findName(), + referenceId: referenceId ?? undefined, }) delegation.delegationScopes = await Promise.all( diff --git a/libs/services/auth/testing/src/fixtures/types.ts b/libs/services/auth/testing/src/fixtures/types.ts index c3f77d51825b..8b96406dd1e3 100644 --- a/libs/services/auth/testing/src/fixtures/types.ts +++ b/libs/services/auth/testing/src/fixtures/types.ts @@ -35,8 +35,11 @@ export type CreateCustomDelegationScope = Optional< 'validFrom' | 'validTo' > export type CreateCustomDelegation = Optional< - Pick, - 'toNationalId' | 'fromNationalId' | 'fromName' + Pick< + DelegationDTO, + 'toNationalId' | 'fromNationalId' | 'fromName' | 'referenceId' + >, + 'toNationalId' | 'fromNationalId' | 'fromName' | 'referenceId' > & { domainName: string scopes?: CreateCustomDelegationScope[] From d5749493ae5f25855f67bbc4e7ddd4fee2030d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Thu, 26 Sep 2024 11:33:55 +0200 Subject: [PATCH 145/173] fix(j-s): Merge Case Access (#16053) * Fixes db queries * Shows merged cases to prosecutors and defenders * Only returns completed indictments that ended with a fine or a ruling and have been sent to the public prosecutor * Updates unit tests * Opens case files from merged cases * Opens generated indictments and case files records of merged cases * Updates unit tests * Removes console log * Fixes broken web build * Fixes accordion declarations and adds comments --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../app/modules/backend/backend.service.ts | 21 ++++- .../modules/file/dto/getSignedUrl.input.ts | 7 +- .../src/app/modules/file/file.controller.ts | 21 ++++- .../api/src/app/modules/file/file.resolver.ts | 4 +- .../file/limitedAccessFile.controller.ts | 21 ++++- .../file/limitedAccessFile.resolver.ts | 8 +- .../src/app/modules/case/case.controller.ts | 13 +++- .../src/app/modules/case/case.service.ts | 7 +- .../app/modules/case/filters/case.filter.ts | 22 ++++++ .../app/modules/case/filters/cases.filter.ts | 24 ++++-- .../case/filters/test/cases.filter.spec.ts | 20 ++--- .../case/guards/mergedCaseExists.guard.ts | 45 +++++++++++ .../case/limitedAccessCase.controller.ts | 13 +++- .../modules/case/limitedAccessCase.service.ts | 33 ++++++++ .../getCaseFilesRecordPdfGuards.spec.ts | 4 +- .../getIndictmentPdfGuards.spec.ts | 77 +++---------------- .../getCaseFilesRecordPdfGuards.spec.ts | 4 +- .../getIndictmentPdfGuards.spec.ts | 4 +- .../src/app/modules/file/file.controller.ts | 4 +- .../file/limitedAccessFile.controller.ts | 10 ++- .../getCaseFileSignedUrlGuards.spec.ts | 71 +++-------------- .../getCaseFileSignedUrlGuards.spec.ts | 8 +- .../ConnectedCaseFilesAccordionItem.tsx | 7 +- .../AppealCaseFilesOverview.tsx | 1 - .../src/components/FormProvider/case.graphql | 7 +- .../FormProvider/limitedAccessCase.graphql | 39 +++++++++- .../IndictmentCaseFilesList.tsx | 57 ++++---------- .../src/components/PdfButton/PdfButton.tsx | 14 +++- .../Court/Indictments/Completed/Completed.tsx | 34 ++++++-- .../Court/Indictments/Overview/Overview.tsx | 21 +++-- .../Court/Indictments/Summary/Summary.tsx | 33 ++++---- .../IndictmentOverview/IndictmentOverview.tsx | 1 - .../Indictments/Overview/Overview.tsx | 24 +++++- .../IndictmentOverview/IndictmentOverview.tsx | 25 +++++- .../web/src/utils/hooks/useFileList/index.ts | 21 ++++- 35 files changed, 458 insertions(+), 267 deletions(-) create mode 100644 apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts diff --git a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts index 238f4b51de76..5a42b29ab8a7 100644 --- a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts +++ b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts @@ -269,8 +269,16 @@ export class BackendService extends DataSource<{ req: Request }> { return this.post(`case/${id}/file`, createFile) } - getCaseFileSignedUrl(caseId: string, id: string): Promise { - return this.get(`case/${caseId}/file/${id}/url`) + getCaseFileSignedUrl( + caseId: string, + id: string, + mergedCaseId?: string, + ): Promise { + const mergedCaseInjection = mergedCaseId + ? `/mergedCase/${mergedCaseId}` + : '' + + return this.get(`case/${caseId}${mergedCaseInjection}/file/${id}/url`) } deleteCaseFile(caseId: string, id: string): Promise { @@ -426,8 +434,15 @@ export class BackendService extends DataSource<{ req: Request }> { limitedAccessGetCaseFileSignedUrl( caseId: string, id: string, + mergedCaseId?: string, ): Promise { - return this.get(`case/${caseId}/limitedAccess/file/${id}/url`) + const mergedCaseInjection = mergedCaseId + ? `/mergedCase/${mergedCaseId}` + : '' + + return this.get( + `case/${caseId}/limitedAccess${mergedCaseInjection}/file/${id}/url`, + ) } limitedAccessDeleteCaseFile( diff --git a/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts b/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts index 0718c486c7d8..23bce59da4a7 100644 --- a/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts +++ b/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts @@ -1,4 +1,4 @@ -import { Allow } from 'class-validator' +import { Allow, IsOptional } from 'class-validator' import { Field, ID, InputType } from '@nestjs/graphql' @@ -11,4 +11,9 @@ export class GetSignedUrlInput { @Allow() @Field(() => ID) readonly caseId!: string + + @Allow() + @IsOptional() + @Field(() => ID, { nullable: true }) + readonly mergedCaseId?: string } diff --git a/apps/judicial-system/api/src/app/modules/file/file.controller.ts b/apps/judicial-system/api/src/app/modules/file/file.controller.ts index ab341cdd1364..34e61e6ad93c 100644 --- a/apps/judicial-system/api/src/app/modules/file/file.controller.ts +++ b/apps/judicial-system/api/src/app/modules/file/file.controller.ts @@ -54,10 +54,14 @@ export class FileController { ) } - @Get('caseFilesRecord/:policeCaseNumber') + @Get([ + 'caseFilesRecord/:policeCaseNumber', + 'mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber', + ]) @Header('Content-Type', 'application/pdf') getCaseFilesRecordPdf( @Param('id') id: string, + @Param('mergedCaseId') mergedCaseId: string, @Param('policeCaseNumber') policeCaseNumber: string, @CurrentHttpUser() user: User, @Req() req: Request, @@ -65,11 +69,15 @@ export class FileController { ): Promise { this.logger.debug(`Getting the case files for case ${id} as a pdf document`) + const mergedCaseInjection = mergedCaseId + ? `mergedCase/${mergedCaseId}/` + : '' + return this.fileService.tryGetFile( user.id, AuditedAction.GET_CASE_FILES_PDF, id, - `caseFilesRecord/${policeCaseNumber}`, + `${mergedCaseInjection}caseFilesRecord/${policeCaseNumber}`, req, res, 'pdf', @@ -143,21 +151,26 @@ export class FileController { ) } - @Get('indictment') + @Get(['indictment', 'mergedCase/:mergedCaseId/indictment']) @Header('Content-Type', 'application/pdf') getIndictmentPdf( @Param('id') id: string, + @Param('mergedCaseId') mergedCaseId: string, @CurrentHttpUser() user: User, @Req() req: Request, @Res() res: Response, ): Promise { this.logger.debug(`Getting the indictment for case ${id} as a pdf document`) + const mergedCaseInjection = mergedCaseId + ? `mergedCase/${mergedCaseId}/` + : '' + return this.fileService.tryGetFile( user.id, AuditedAction.GET_INDICTMENT_PDF, id, - 'indictment', + `${mergedCaseInjection}indictment`, req, res, 'pdf', diff --git a/apps/judicial-system/api/src/app/modules/file/file.resolver.ts b/apps/judicial-system/api/src/app/modules/file/file.resolver.ts index 574b00b67e1d..45990fbb7dc2 100644 --- a/apps/judicial-system/api/src/app/modules/file/file.resolver.ts +++ b/apps/judicial-system/api/src/app/modules/file/file.resolver.ts @@ -85,14 +85,14 @@ export class FileResolver { @Context('dataSources') { backendService }: { backendService: BackendService }, ): Promise { - const { caseId, id } = input + const { caseId, id, mergedCaseId } = input this.logger.debug(`Getting a signed url for file ${id} of case ${caseId}`) return this.auditTrailService.audit( user.id, AuditedAction.GET_SIGNED_URL, - backendService.getCaseFileSignedUrl(caseId, id), + backendService.getCaseFileSignedUrl(caseId, id, mergedCaseId), id, ) } diff --git a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts index f33dc4a3d644..247f47218ef4 100644 --- a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts +++ b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts @@ -53,10 +53,14 @@ export class LimitedAccessFileController { ) } - @Get('caseFilesRecord/:policeCaseNumber') + @Get([ + 'caseFilesRecord/:policeCaseNumber', + 'mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber', + ]) @Header('Content-Type', 'application/pdf') async getCaseFilesRecordPdf( @Param('id') id: string, + @Param('mergedCaseId') mergedCaseId: string, @Param('policeCaseNumber') policeCaseNumber: string, @CurrentHttpUser() user: User, @Req() req: Request, @@ -64,11 +68,15 @@ export class LimitedAccessFileController { ): Promise { this.logger.debug(`Getting the case files for case ${id} as a pdf document`) + const mergedCaseInjection = mergedCaseId + ? `mergedCase/${mergedCaseId}/` + : '' + return this.fileService.tryGetFile( user.id, AuditedAction.GET_CASE_FILES_PDF, id, - `limitedAccess/caseFilesRecord/${policeCaseNumber}`, + `limitedAccess/${mergedCaseInjection}caseFilesRecord/${policeCaseNumber}`, req, res, 'pdf', @@ -142,21 +150,26 @@ export class LimitedAccessFileController { ) } - @Get('indictment') + @Get(['indictment', 'mergedCase/:mergedCaseId/indictment']) @Header('Content-Type', 'application/pdf') async getIndictmentPdf( @Param('id') id: string, + @Param('mergedCaseId') mergedCaseId: string, @CurrentHttpUser() user: User, @Req() req: Request, @Res() res: Response, ): Promise { this.logger.debug(`Getting the indictment for case ${id} as a pdf document`) + const mergedCaseInjection = mergedCaseId + ? `mergedCase/${mergedCaseId}/` + : '' + return this.fileService.tryGetFile( user.id, AuditedAction.GET_INDICTMENT_PDF, id, - 'limitedAccess/indictment', + `limitedAccess/${mergedCaseInjection}indictment`, req, res, 'pdf', diff --git a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts index 2d6fb1e19d0c..ecd4c88ec4bf 100644 --- a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts +++ b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts @@ -84,14 +84,18 @@ export class LimitedAccessFileResolver { @Context('dataSources') { backendService }: { backendService: BackendService }, ): Promise { - const { caseId, id } = input + const { caseId, id, mergedCaseId } = input this.logger.debug(`Getting a signed url for file ${id} of case ${caseId}`) return this.auditTrailService.audit( user.id, AuditedAction.GET_SIGNED_URL, - backendService.limitedAccessGetCaseFileSignedUrl(caseId, id), + backendService.limitedAccessGetCaseFileSignedUrl( + caseId, + id, + mergedCaseId, + ), id, ) } diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts index 9efb4338c21f..084aefd70922 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts @@ -79,6 +79,7 @@ import { CaseExistsGuard } from './guards/caseExists.guard' import { CaseReadGuard } from './guards/caseRead.guard' import { CaseTypeGuard } from './guards/caseType.guard' import { CaseWriteGuard } from './guards/caseWrite.guard' +import { MergedCaseExistsGuard } from './guards/mergedCaseExists.guard' import { courtOfAppealsAssistantTransitionRule, courtOfAppealsAssistantUpdateRule, @@ -549,6 +550,7 @@ export class CaseController { CaseExistsGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, + MergedCaseExistsGuard, ) @RolesRules( prosecutorRule, @@ -558,7 +560,10 @@ export class CaseController { districtCourtRegistrarRule, districtCourtAssistantRule, ) - @Get('case/:caseId/caseFilesRecord/:policeCaseNumber') + @Get([ + 'case/:caseId/caseFilesRecord/:policeCaseNumber', + 'case/:caseId/mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber', + ]) @ApiOkResponse({ content: { 'application/pdf': {} }, description: @@ -705,6 +710,7 @@ export class CaseController { CaseExistsGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, + MergedCaseExistsGuard, ) @RolesRules( prosecutorRule, @@ -714,7 +720,10 @@ export class CaseController { districtCourtRegistrarRule, districtCourtAssistantRule, ) - @Get('case/:caseId/indictment') + @Get([ + 'case/:caseId/indictment', + 'case/:caseId/mergedCase/:mergedCaseId/indictment', + ]) @Header('Content-Type', 'application/pdf') @ApiOkResponse({ content: { 'application/pdf': {} }, 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 1d4043abd85d..60896a1422b8 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 @@ -321,6 +321,10 @@ export const include: Includeable[] = [ CaseFileCategory.CRIMINAL_RECORD, CaseFileCategory.COST_BREAKDOWN, CaseFileCategory.CRIMINAL_RECORD_UPDATE, + CaseFileCategory.CASE_FILE, + CaseFileCategory.PROSECUTOR_CASE_FILE, + CaseFileCategory.DEFENDANT_CASE_FILE, + CaseFileCategory.CIVIL_CLAIM, ], }, }, @@ -388,14 +392,13 @@ export const caseListInclude: Includeable[] = [ as: 'eventLogs', required: false, where: { eventType: { [Op.in]: eventTypes } }, - order: [['created', 'DESC']], - separate: true, }, ] export const listOrder: OrderItem[] = [ [{ model: Defendant, as: 'defendants' }, 'created', 'ASC'], [{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC'], + [{ model: EventLog, as: 'eventLogs' }, 'created', 'DESC'], ] @Injectable() diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts index dfb9f7bc678f..59506016d28b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts @@ -6,6 +6,7 @@ import { CaseIndictmentRulingDecision, CaseState, CaseType, + EventType, getIndictmentVerdictAppealDeadlineStatus, IndictmentCaseReviewDecision, isCourtOfAppealsUser, @@ -87,6 +88,27 @@ const canPublicProsecutionUserAccessCase = (theCase: Case): boolean => { return false } + // Check indictment ruling decision access + if ( + !theCase.indictmentRulingDecision || + ![ + CaseIndictmentRulingDecision.FINE, + CaseIndictmentRulingDecision.RULING, + ].includes(theCase.indictmentRulingDecision) + ) { + return false + } + + // Make sure the indictment has been sent to the public prosecutor + if ( + !theCase.eventLogs?.some( + (eventLog) => + eventLog.eventType === EventType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR, + ) + ) { + return false + } + return true } diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts index 0ccb2aa3c953..c0df2270d241 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts @@ -11,6 +11,7 @@ import { CaseState, CaseType, DateType, + EventType, IndictmentCaseReviewDecision, indictmentCases, investigationCases, @@ -75,8 +76,20 @@ const getPublicProsecutionUserCasesQueryFilter = (): WhereOptions => { return { [Op.and]: [ { is_archived: false }, - { state: [CaseState.COMPLETED] }, { type: indictmentCases }, + { state: CaseState.COMPLETED }, + { + indictment_ruling_decision: [ + CaseIndictmentRulingDecision.FINE, + CaseIndictmentRulingDecision.RULING, + ], + }, + { + // The following condition will filter out all event logs that are not of type INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR + // but that should be ok the case list for the public prosecutor is not using other event logs + '$eventLogs.event_type$': + EventType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR, + }, ], } } @@ -190,15 +203,10 @@ const getPrisonAdminUserCasesQueryFilter = (): WhereOptions => { [Op.or]: [ { state: CaseState.ACCEPTED, - type: [ - CaseType.CUSTODY, - CaseType.ADMISSION_TO_FACILITY, - CaseType.PAROLE_REVOCATION, - CaseType.TRAVEL_BAN, - ], + type: [...restrictionCases, CaseType.PAROLE_REVOCATION], }, { - type: CaseType.INDICTMENT, + type: indictmentCases, state: CaseState.COMPLETED, indictment_ruling_decision: CaseIndictmentRulingDecision.RULING, indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts index 0e2217081425..65c759346088 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts @@ -12,6 +12,7 @@ import { courtOfAppealsRoles, DateType, districtCourtRoles, + EventType, IndictmentCaseReviewDecision, indictmentCases, InstitutionType, @@ -306,11 +307,17 @@ describe('getCasesQueryFilter', () => { expect(res).toStrictEqual({ [Op.and]: [ { is_archived: false }, + { type: indictmentCases }, + { state: CaseState.COMPLETED }, { - state: [CaseState.COMPLETED], + indictment_ruling_decision: [ + CaseIndictmentRulingDecision.FINE, + CaseIndictmentRulingDecision.RULING, + ], }, { - type: indictmentCases, + '$eventLogs.event_type$': + EventType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR, }, ], }) @@ -371,15 +378,10 @@ describe('getCasesQueryFilter', () => { [Op.or]: [ { state: CaseState.ACCEPTED, - type: [ - CaseType.CUSTODY, - CaseType.ADMISSION_TO_FACILITY, - CaseType.PAROLE_REVOCATION, - CaseType.TRAVEL_BAN, - ], + type: [...restrictionCases, CaseType.PAROLE_REVOCATION], }, { - type: CaseType.INDICTMENT, + type: indictmentCases, state: CaseState.COMPLETED, indictment_ruling_decision: CaseIndictmentRulingDecision.RULING, indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT, diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts b/apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts new file mode 100644 index 000000000000..2347b578d010 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts @@ -0,0 +1,45 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, + InternalServerErrorException, +} from '@nestjs/common' + +import { CaseService } from '../case.service' +import { Case } from '../models/case.model' + +@Injectable() +export class MergedCaseExistsGuard implements CanActivate { + constructor(private readonly caseService: CaseService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest() + + const mergedCaseId = request.params.mergedCaseId + + // If the user is not accessing a merged case, we don't need to do anything + if (!mergedCaseId) { + return true + } + + const theCase: Case = request.case + + if (!theCase) { + throw new InternalServerErrorException('Missing case') + } + + const mergedCase = theCase.mergedCases?.find( + (mergedCase) => mergedCase.id === mergedCaseId, + ) + + if (!mergedCase) { + throw new BadRequestException('Merged case not found') + } + + request.params.caseId = mergedCaseId + request.case = mergedCase + + return true + } +} diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts index 8f73eae0f43b..aa1a56f603ae 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts @@ -51,6 +51,7 @@ import { CaseReadGuard } from './guards/caseRead.guard' import { CaseTypeGuard } from './guards/caseType.guard' import { CaseWriteGuard } from './guards/caseWrite.guard' import { LimitedAccessCaseExistsGuard } from './guards/limitedAccessCaseExists.guard' +import { MergedCaseExistsGuard } from './guards/mergedCaseExists.guard' import { RequestSharedWithDefenderGuard } from './guards/requestSharedWithDefender.guard' import { defenderTransitionRule, defenderUpdateRule } from './guards/rolesRules' import { CaseInterceptor } from './interceptors/case.interceptor' @@ -243,9 +244,13 @@ export class LimitedAccessCaseController { CaseExistsGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, + MergedCaseExistsGuard, ) @RolesRules(defenderRule) - @Get('case/:caseId/limitedAccess/caseFilesRecord/:policeCaseNumber') + @Get([ + 'case/:caseId/limitedAccess/caseFilesRecord/:policeCaseNumber', + 'case/:caseId/limitedAccess/mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber', + ]) @ApiOkResponse({ content: { 'application/pdf': {} }, description: @@ -374,9 +379,13 @@ export class LimitedAccessCaseController { CaseExistsGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, + MergedCaseExistsGuard, ) @RolesRules(defenderRule) - @Get('case/:caseId/limitedAccess/indictment') + @Get([ + 'case/:caseId/limitedAccess/indictment', + 'case/:caseId/limitedAccess/mergedCase/:mergedCaseId/indictment', + ]) @Header('Content-Type', 'application/pdf') @ApiOkResponse({ content: { 'application/pdf': {} }, diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index 9dfa815c7d73..fa2e9b5bbaeb 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -36,6 +36,7 @@ import { CaseFile, defenderCaseFileCategoriesForRestrictionAndInvestigationCases, } from '../file' +import { IndictmentCount } from '../indictment-count' import { Institution } from '../institution' import { User } from '../user' import { Case } from './models/case.model' @@ -170,6 +171,7 @@ export const include: Includeable[] = [ { model: Case, as: 'parentCase', attributes }, { model: Case, as: 'childCase', attributes }, { model: Defendant, as: 'defendants' }, + { model: IndictmentCount, as: 'indictmentCounts' }, { model: CivilClaimant, as: 'civilClaimants' }, { model: CaseFile, @@ -220,10 +222,41 @@ export const include: Includeable[] = [ where: { stringType: { [Op.in]: stringTypes } }, }, { model: Case, as: 'mergeCase', attributes }, + { + model: Case, + as: 'mergedCases', + where: { state: CaseState.COMPLETED }, + include: [ + { + model: CaseFile, + as: 'caseFiles', + required: false, + where: { + state: { [Op.not]: CaseFileState.DELETED }, + category: { + [Op.in]: [ + CaseFileCategory.INDICTMENT, + CaseFileCategory.COURT_RECORD, + CaseFileCategory.CRIMINAL_RECORD, + CaseFileCategory.COST_BREAKDOWN, + CaseFileCategory.CRIMINAL_RECORD_UPDATE, + CaseFileCategory.CASE_FILE, + CaseFileCategory.PROSECUTOR_CASE_FILE, + CaseFileCategory.DEFENDANT_CASE_FILE, + CaseFileCategory.CIVIL_CLAIM, + ], + }, + }, + separate: true, + }, + ], + separate: true, + }, ] export const order: OrderItem[] = [ [{ model: Defendant, as: 'defendants' }, 'created', 'ASC'], + [{ model: IndictmentCount, as: 'indictmentCounts' }, 'created', 'ASC'], [{ model: CivilClaimant, as: 'civilClaimants' }, 'created', 'ASC'], [{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC'], ] diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts index f59fec872cbf..1647704e5a27 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts @@ -5,6 +5,7 @@ import { CaseController } from '../../case.controller' import { CaseExistsGuard } from '../../guards/caseExists.guard' import { CaseReadGuard } from '../../guards/caseRead.guard' import { CaseTypeGuard } from '../../guards/caseType.guard' +import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard' describe('CaseController - Get case files record pdf guards', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -18,7 +19,7 @@ describe('CaseController - Get case files record pdf guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(5) + expect(guards).toHaveLength(6) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) expect(new guards[1]()).toBeInstanceOf(RolesGuard) expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) @@ -27,5 +28,6 @@ describe('CaseController - Get case files record pdf guards', () => { allowedCaseTypes: indictmentCases, }) expect(new guards[4]()).toBeInstanceOf(CaseReadGuard) + expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts index fa644b448dc1..d86a9827ce80 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts @@ -7,6 +7,7 @@ import { CaseController } from '../../case.controller' import { CaseExistsGuard } from '../../guards/caseExists.guard' import { CaseReadGuard } from '../../guards/caseRead.guard' import { CaseTypeGuard } from '../../guards/caseType.guard' +import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard' describe('CaseController - Get indictment pdf guards', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -19,70 +20,16 @@ describe('CaseController - Get indictment pdf guards', () => { ) }) - it('should have five guards', () => { - expect(guards).toHaveLength(5) - }) - - describe('JwtAuthGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[0]() - }) - - it('should have JwtAuthGuard as guard 1', () => { - expect(guard).toBeInstanceOf(JwtAuthGuard) - }) - }) - - describe('RolesGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[1]() - }) - - it('should have RolesGuard as guard 2', () => { - expect(guard).toBeInstanceOf(RolesGuard) - }) - }) - - describe('CaseExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[2]() - }) - - it('should have CaseExistsGuard as guard 3', () => { - expect(guard).toBeInstanceOf(CaseExistsGuard) - }) - }) - - describe('CaseTypeGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = guards[3] - }) - - it('should have CaseTypeGuard as guard 4', () => { - expect(guard).toBeInstanceOf(CaseTypeGuard) - expect(guard).toEqual({ - allowedCaseTypes: indictmentCases, - }) - }) - }) - - describe('CaseReadGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[4]() - }) - - it('should have CaseReadGuard as guard 5', () => { - expect(guard).toBeInstanceOf(CaseReadGuard) - }) + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(6) + expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) + expect(new guards[1]()).toBeInstanceOf(RolesGuard) + expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[3]).toBeInstanceOf(CaseTypeGuard) + expect(guards[3]).toEqual({ + allowedCaseTypes: indictmentCases, + }) + expect(new guards[4]()).toBeInstanceOf(CaseReadGuard) + expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts index 837fb66529ea..f9cf460fb962 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts @@ -4,6 +4,7 @@ import { indictmentCases } from '@island.is/judicial-system/types' import { CaseExistsGuard } from '../../guards/caseExists.guard' import { CaseReadGuard } from '../../guards/caseRead.guard' import { CaseTypeGuard } from '../../guards/caseType.guard' +import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard' import { LimitedAccessCaseController } from '../../limitedAccessCase.controller' describe('LimitedAccessCaseController - Get case files record pdf guards', () => { @@ -18,7 +19,7 @@ describe('LimitedAccessCaseController - Get case files record pdf guards', () => }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(5) + expect(guards).toHaveLength(6) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) expect(new guards[1]()).toBeInstanceOf(RolesGuard) expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) @@ -27,5 +28,6 @@ describe('LimitedAccessCaseController - Get case files record pdf guards', () => allowedCaseTypes: indictmentCases, }) expect(new guards[4]()).toBeInstanceOf(CaseReadGuard) + expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts index 3b375c1ce78d..bc2a8f2e1546 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts @@ -4,6 +4,7 @@ import { indictmentCases } from '@island.is/judicial-system/types' import { CaseExistsGuard } from '../../guards/caseExists.guard' import { CaseReadGuard } from '../../guards/caseRead.guard' import { CaseTypeGuard } from '../../guards/caseType.guard' +import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard' import { LimitedAccessCaseController } from '../../limitedAccessCase.controller' describe('LimitedAccessCaseController - Get indictment pdf guards', () => { @@ -18,7 +19,7 @@ describe('LimitedAccessCaseController - Get indictment pdf guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(5) + expect(guards).toHaveLength(6) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) expect(new guards[1]()).toBeInstanceOf(RolesGuard) expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) @@ -27,5 +28,6 @@ describe('LimitedAccessCaseController - Get indictment pdf guards', () => { allowedCaseTypes: indictmentCases, }) expect(new guards[4]()).toBeInstanceOf(CaseReadGuard) + expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts index 340d782f767b..abbd76f30659 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts @@ -49,6 +49,7 @@ import { CaseWriteGuard, CurrentCase, } from '../case' +import { MergedCaseExistsGuard } from '../case/guards/mergedCaseExists.guard' import { CreateFileDto } from './dto/createFile.dto' import { CreatePresignedPostDto } from './dto/createPresignedPost.dto' import { UpdateFilesDto } from './dto/updateFile.dto' @@ -128,6 +129,7 @@ export class FileController { RolesGuard, CaseExistsGuard, CaseReadGuard, + MergedCaseExistsGuard, CaseFileExistsGuard, ViewCaseFileGuard, ) @@ -143,7 +145,7 @@ export class FileController { courtOfAppealsAssistantRule, prisonSystemStaffRule, ) - @Get('file/:fileId/url') + @Get(['file/:fileId/url', 'mergedCase/:mergedCaseId/file/:fileId/url']) @ApiOkResponse({ type: SignedUrl, description: 'Gets a signed url for a case file', diff --git a/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts b/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts index 395a9ef78d84..3c1605da9c12 100644 --- a/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts @@ -36,6 +36,7 @@ import { CurrentCase, LimitedAccessCaseExistsGuard, } from '../case' +import { MergedCaseExistsGuard } from '../case/guards/mergedCaseExists.guard' import { CreateFileDto } from './dto/createFile.dto' import { CreatePresignedPostDto } from './dto/createPresignedPost.dto' import { CurrentCaseFile } from './guards/caseFile.decorator' @@ -107,9 +108,14 @@ export class LimitedAccessFileController { return this.fileService.createCaseFile(theCase, createFile, user) } - @UseGuards(CaseReadGuard, CaseFileExistsGuard, LimitedAccessViewCaseFileGuard) + @UseGuards( + CaseReadGuard, + MergedCaseExistsGuard, + CaseFileExistsGuard, + LimitedAccessViewCaseFileGuard, + ) @RolesRules(prisonSystemStaffRule, defenderRule) - @Get('file/:fileId/url') + @Get(['file/:fileId/url', 'mergedCase/:mergedCaseId/file/:fileId/url']) @ApiOkResponse({ type: SignedUrl, description: 'Gets a signed url for a case file', diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts index 009ce1aba49e..88c3d0946486 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts @@ -3,6 +3,7 @@ import { CanActivate } from '@nestjs/common' import { RolesGuard } from '@island.is/judicial-system/auth' import { CaseExistsGuard, CaseReadGuard } from '../../../case' +import { MergedCaseExistsGuard } from '../../../case/guards/mergedCaseExists.guard' import { FileController } from '../../file.controller' import { CaseFileExistsGuard } from '../../guards/caseFileExists.guard' import { ViewCaseFileGuard } from '../../guards/viewCaseFile.guard' @@ -18,67 +19,13 @@ describe('FileController - Get case file signed url guards', () => { ) }) - it('should have five guards', () => { - expect(guards).toHaveLength(5) - }) - - describe('RolesGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[0]() - }) - - it('should have RolesGuard as guard 1', () => { - expect(guard).toBeInstanceOf(RolesGuard) - }) - }) - - describe('CaseExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[1]() - }) - - it('should have CaseExistsGuard as guard 2', () => { - expect(guard).toBeInstanceOf(CaseExistsGuard) - }) - }) - - describe('CaseReadGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[2]() - }) - - it('should have CaseReadGuard as guard 3', () => { - expect(guard).toBeInstanceOf(CaseReadGuard) - }) - }) - - describe('CaseFileExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[3]() - }) - - it('should have CaseFileExistsGuard as guard 4', () => { - expect(guard).toBeInstanceOf(CaseFileExistsGuard) - }) - }) - - describe('ViewCaseFileGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[4]() - }) - - it('should have ViewCaseFileGuard as guard 5', () => { - expect(guard).toBeInstanceOf(ViewCaseFileGuard) - }) + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(6) + expect(new guards[0]()).toBeInstanceOf(RolesGuard) + expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[2]()).toBeInstanceOf(CaseReadGuard) + expect(new guards[3]()).toBeInstanceOf(MergedCaseExistsGuard) + expect(new guards[4]()).toBeInstanceOf(CaseFileExistsGuard) + expect(new guards[5]()).toBeInstanceOf(ViewCaseFileGuard) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts index bcd54dc4ef49..93fad8b53589 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts @@ -1,4 +1,5 @@ import { CaseReadGuard } from '../../../case' +import { MergedCaseExistsGuard } from '../../../case/guards/mergedCaseExists.guard' import { CaseFileExistsGuard } from '../../guards/caseFileExists.guard' import { LimitedAccessViewCaseFileGuard } from '../../guards/limitedAccessViewCaseFile.guard' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' @@ -15,9 +16,10 @@ describe('LimitedAccessFileController - Get case file signed url guards', () => }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(3) + expect(guards).toHaveLength(4) expect(new guards[0]()).toBeInstanceOf(CaseReadGuard) - expect(new guards[1]()).toBeInstanceOf(CaseFileExistsGuard) - expect(new guards[2]()).toBeInstanceOf(LimitedAccessViewCaseFileGuard) + expect(new guards[1]()).toBeInstanceOf(MergedCaseExistsGuard) + expect(new guards[2]()).toBeInstanceOf(CaseFileExistsGuard) + expect(new guards[3]()).toBeInstanceOf(LimitedAccessViewCaseFileGuard) }) }) diff --git a/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx b/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx index c65bf82aa94c..631bf925ccf3 100644 --- a/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx +++ b/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx @@ -8,10 +8,14 @@ import { Case } from '@island.is/judicial-system-web/src/graphql/schema' import { strings } from './ConnectedCaseFilesAccordionItem.strings' interface Props { + connectedCaseParentId: string connectedCase: Case } -const ConnectedCaseFilesAccordionItem: FC = ({ connectedCase }) => { +const ConnectedCaseFilesAccordionItem: FC = ({ + connectedCaseParentId, + connectedCase, +}) => { const { formatMessage } = useIntl() const { caseFiles, courtCaseNumber } = connectedCase @@ -30,6 +34,7 @@ const ConnectedCaseFilesAccordionItem: FC = ({ connectedCase }) => { ) diff --git a/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx b/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx index 843a2bfdb20f..2b49c6b6e017 100644 --- a/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx +++ b/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx @@ -127,7 +127,6 @@ const AppealCaseFilesOverview = () => { onOpen(file.id)} diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index 70cdc86833c7..f6eeefc5ba75 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -299,11 +299,16 @@ query Case($input: CaseQueryInput!) { caseFiles { id created + modified name + type + category state key size - category + userGeneratedFilename + displayDate + submittedBy } policeCaseNumbers indictmentSubtypes diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index f9ff9d254cec..774715ba84f3 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -10,10 +10,13 @@ query LimitedAccessCase($input: CaseQueryInput!) { caseFiles { id created + modified name + type category + state key - policeCaseNumber + size userGeneratedFilename displayDate submittedBy @@ -165,6 +168,40 @@ query LimitedAccessCase($input: CaseQueryInput!) { id courtCaseNumber } + indictmentCounts { + id + caseId + policeCaseNumber + created + modified + vehicleRegistrationNumber + offenses + substances + lawsBroken + incidentDescription + legalArguments + } + mergedCases { + id + courtCaseNumber + type + caseFiles { + id + created + modified + name + type + category + state + key + size + userGeneratedFilename + displayDate + submittedBy + } + policeCaseNumbers + indictmentSubtypes + } hasCivilClaims civilClaimants { id diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index 18b09ea27a52..99d714df2a3e 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -32,6 +32,7 @@ import { strings } from './IndictmentCaseFilesList.strings' interface Props { workingCase: Case displayHeading?: boolean + connectedCaseParentId?: string } interface RenderFilesProps { @@ -39,17 +40,15 @@ interface RenderFilesProps { onOpenFile: (fileId: string) => void } -export const RenderFiles: FC = ({ +export const RenderFiles: FC = ({ caseFiles, onOpenFile, - workingCase, }) => { return ( <> {caseFiles.map((file) => ( = ({ const IndictmentCaseFilesList: FC = ({ workingCase, displayHeading = true, + connectedCaseParentId, }) => { const { formatMessage } = useIntl() const { user } = useContext(UserContext) const { onOpen, fileNotFound, dismissFileNotFound } = useFileList({ caseId: workingCase.id, + connectedCaseParentId, }) const showTrafficViolationCaseFiles = isTrafficViolationCase(workingCase) @@ -116,11 +117,7 @@ const IndictmentCaseFilesList: FC = ({ {formatMessage(caseFiles.indictmentSection)} - + )} {showTrafficViolationCaseFiles && ( @@ -131,6 +128,7 @@ const IndictmentCaseFilesList: FC = ({ = ({ {formatMessage(caseFiles.criminalRecordSection)} - + )} {criminalRecordUpdate && @@ -159,11 +153,7 @@ const IndictmentCaseFilesList: FC = ({ {formatMessage(caseFiles.criminalRecordUpdateSection)} - + )} {costBreakdowns && costBreakdowns.length > 0 && ( @@ -171,11 +161,7 @@ const IndictmentCaseFilesList: FC = ({ {formatMessage(caseFiles.costBreakdownSection)} - + )} {others && others.length > 0 && ( @@ -183,11 +169,7 @@ const IndictmentCaseFilesList: FC = ({ {formatMessage(caseFiles.otherDocumentsSection)} - + )} @@ -198,6 +180,7 @@ const IndictmentCaseFilesList: FC = ({ = ({ {formatMessage(strings.rulingAndCourtRecordsTitle)} {courtRecords && courtRecords.length > 0 && ( - + )} {(isDistrictCourtUser(user) || isCompletedCase(workingCase.state)) && rulings && rulings.length > 0 && ( - + )} ) : null} @@ -241,11 +216,7 @@ const IndictmentCaseFilesList: FC = ({ {formatMessage(strings.civilClaimsTitle)} - + )} {uploadedCaseFiles && uploadedCaseFiles.length > 0 && ( diff --git a/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx b/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx index 93f455330ffa..8ff7a5bd0fb0 100644 --- a/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx +++ b/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx @@ -7,7 +7,8 @@ import { UserContext } from '../UserProvider/UserProvider' import * as styles from './PdfButton.css' interface Props { - caseId: string + caseId?: string + connectedCaseParentId?: string title?: string | null pdfType?: | 'ruling' @@ -27,6 +28,9 @@ interface Props { const PdfButton: FC> = ({ caseId, + // This is used when accessing data belonging to a case which has been merged into another case. + // For access control purposes, the data must be accessed through the parent case. + connectedCaseParentId, title, pdfType, disabled, @@ -39,10 +43,14 @@ const PdfButton: FC> = ({ const { limitedAccess } = useContext(UserContext) const handlePdfClick = async () => { - const prefix = limitedAccess ? 'limitedAccess/' : '' + const prefix = `${limitedAccess ? 'limitedAccess/' : ''}${ + connectedCaseParentId ? `mergedCase/${caseId}/` : '' + }` const postfix = elementId ? `/${elementId}` : '' const query = queryParameters ? `?${queryParameters}` : '' - const url = `${api.apiUrl}/api/case/${caseId}/${prefix}${pdfType}${postfix}${query}` + const url = `${api.apiUrl}/api/case/${ + connectedCaseParentId ?? caseId + }/${prefix}${pdfType}${postfix}${query}` window.open(url, '_blank') } diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx index 4c407547d670..9d9383794e44 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx @@ -3,6 +3,7 @@ import { useIntl } from 'react-intl' import router from 'next/router' import { + Accordion, Box, InputFileUpload, RadioButton, @@ -18,11 +19,13 @@ import { FormContext, FormFooter, IndictmentCaseFilesList, + IndictmentsLawsBrokenAccordionItem, InfoCardClosedIndictment, Modal, PageHeader, PageLayout, SectionHeading, + useIndictmentsLawsBroken, } from '@island.is/judicial-system-web/src/components' import { CaseFileCategory, @@ -48,6 +51,7 @@ const Completed: FC = () => { useUploadFiles(workingCase.caseFiles) const { handleUpload, handleRemove } = useS3Upload(workingCase.id) const { createEventLog } = useEventLog() + const lawsBroken = useIndictmentsLawsBroken(workingCase) const [modalVisible, setModalVisible] = useState<'SENT_TO_PUBLIC_PROSECUTOR'>() @@ -126,6 +130,10 @@ const Completed: FC = () => { ) : true + const hasLawsBroken = lawsBroken.size > 0 + const hasMergeCases = + workingCase.mergedCases && workingCase.mergedCases.length > 0 + return ( { - {workingCase.mergedCases && - workingCase.mergedCases.length > 0 && - workingCase.mergedCases.map((mergedCase) => ( - - - - ))} + {(hasLawsBroken || hasMergeCases) && ( + + {hasLawsBroken && ( + + )} + {hasMergeCases && ( + + {workingCase.mergedCases?.map((mergedCase) => ( + + + + ))} + + )} + + )} 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 ce7a017ecbcb..0df16e8b0a6d 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 @@ -2,7 +2,7 @@ import { useCallback, useContext, useState } from 'react' import { useIntl } from 'react-intl' import { useRouter } from 'next/router' -import { Box } from '@island.is/island-ui/core' +import { Accordion, Box } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' import { core, titles } from '@island.is/judicial-system-web/messages' import { @@ -101,13 +101,18 @@ const IndictmentOverview = () => { )} - {workingCase.mergedCases && - workingCase.mergedCases.length > 0 && - workingCase.mergedCases.map((mergedCase) => ( - - - - ))} + {workingCase.mergedCases && workingCase.mergedCases.length > 0 && ( + + {workingCase.mergedCases.map((mergedCase) => ( + + + + ))} + + )} {workingCase.caseFiles && ( diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx index f6128bdd3cfe..d1186e033ce4 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx @@ -2,7 +2,7 @@ import { FC, useContext, useState } from 'react' import { useIntl } from 'react-intl' import router from 'next/router' -import { Box, Text } from '@island.is/island-ui/core' +import { Accordion, Box, Text } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' import { core } from '@island.is/judicial-system-web/messages' import { @@ -126,13 +126,18 @@ const Summary: FC = () => { - {workingCase.mergedCases && - workingCase.mergedCases.length > 0 && - workingCase.mergedCases.map((mergedCase) => ( - - - - ))} + {workingCase.mergedCases && workingCase.mergedCases.length > 0 && ( + + {workingCase.mergedCases.map((mergedCase) => ( + + + + ))} + + )} {(rulingFiles.length > 0 || courtRecordFiles.length > 0) && ( @@ -140,18 +145,10 @@ const Summary: FC = () => { {formatMessage(strings.caseFilesSubtitleRuling)} {rulingFiles.length > 0 && ( - + )} {courtRecordFiles.length > 0 && ( - + )} )} diff --git a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx index a0f765f07198..e54977b11363 100644 --- a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx +++ b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx @@ -73,7 +73,6 @@ const IndictmentOverview = () => { {formatMessage(strings.verdictTitle)} { router.push(constants.CASES_ROUTE) } + const hasLawsBroken = lawsBroken.size > 0 + const hasMergeCases = + workingCase.mergedCases && workingCase.mergedCases.length > 0 + return ( { - {lawsBroken.size > 0 && ( + {(hasLawsBroken || hasMergeCases) && ( - + {hasLawsBroken && ( + + )} + {hasMergeCases && ( + + {workingCase.mergedCases?.map((mergedCase) => ( + + + + ))} + + )} )} { [router, workingCase.id], ) + const hasLawsBroken = lawsBroken.size > 0 + const hasMergeCases = + workingCase.mergedCases && workingCase.mergedCases.length > 0 + return ( { )} - {lawsBroken.size > 0 && ( + {(hasLawsBroken || hasMergeCases) && ( - + {hasLawsBroken && ( + + )} + {hasMergeCases && ( + + {workingCase.mergedCases?.map((mergedCase) => ( + + + + ))} + + )} )} {workingCase.caseFiles && ( diff --git a/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts b/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts index 475d9bd11bca..0ab7be7464ed 100644 --- a/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts @@ -14,9 +14,10 @@ import { useLimitedAccessGetSignedUrlLazyQuery } from './limitedAccessGetSigendU interface Parameters { caseId: string + connectedCaseParentId?: string } -const useFileList = ({ caseId }: Parameters) => { +const useFileList = ({ caseId, connectedCaseParentId }: Parameters) => { const { limitedAccess } = useContext(UserContext) const { setWorkingCase } = useContext(FormContext) const { formatMessage } = useIntl() @@ -100,9 +101,23 @@ const useFileList = ({ caseId }: Parameters) => { () => (fileId: string) => { const query = limitedAccess ? limitedAccessGetSignedUrl : getSignedUrl - query({ variables: { input: { id: fileId, caseId } } }) + query({ + variables: { + input: { + id: fileId, + caseId: connectedCaseParentId ?? caseId, + mergedCaseId: connectedCaseParentId && caseId, + }, + }, + }) }, - [caseId, getSignedUrl, limitedAccess, limitedAccessGetSignedUrl], + [ + caseId, + connectedCaseParentId, + getSignedUrl, + limitedAccess, + limitedAccessGetSignedUrl, + ], ) const dismissFileNotFound = () => { From 5828cef3e8c98ce702bb20b5dcb48e6c98c9b8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigr=C3=BAn=20Tinna=20Gissurard=C3=B3ttir?= <39527334+sigruntg@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:13:29 +0000 Subject: [PATCH 146/173] fix(transport-authority): sms and email error message (#16160) * fixing error messages * adding role --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../change-machine-supervisor.utils.ts | 20 +---- .../transfer-of-machine-ownership.service.ts | 60 +++----------- .../change-co-owner-of-vehicle.service.ts | 60 +++----------- .../change-operator-of-vehicle.service.ts | 60 +++----------- .../transfer-of-vehicle-ownership.service.ts | 80 +++---------------- 5 files changed, 42 insertions(+), 238 deletions(-) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts b/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts index 566f17ad80ee..d32c280f7468 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts @@ -74,16 +74,8 @@ export const sendNotificationsToRecipients = async ( ) .catch((e) => { errors.push( - `Error sending email about submit application in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about submit application in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -104,13 +96,7 @@ export const sendNotificationsToRecipients = async ( errors.push( `Error sending sms about submit application to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts index 14a2bb0b4b8e..fba8b32860bb 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts @@ -128,16 +128,8 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe ) .catch((e) => { this.logger.error( - `Error sending email about submit application in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about submit application in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -154,13 +146,7 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe this.logger.error( `Error sending sms about submit application to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -217,16 +203,8 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe ) .catch((e) => { this.logger.error( - `Error sending email about initReview in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about initReview in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -243,13 +221,7 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe this.logger.error( `Error sending sms about initReview to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -333,16 +305,8 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe ) .catch((e) => { this.logger.error( - `Error sending email about rejectApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about rejectApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -363,13 +327,7 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe this.logger.error( `Error sending sms about rejectApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts index 4354a5020f61..3b75a3530164 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts @@ -281,16 +281,8 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about initReview in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about initReview in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -307,13 +299,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { this.logger.error( `Error sending sms about initReview to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -355,16 +341,8 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about rejectApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about rejectApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -385,13 +363,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { this.logger.error( `Error sending sms about rejectApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -512,16 +484,8 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { ) .catch(() => { this.logger.error( - `Error sending email about submitApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about submitApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, ) }) } @@ -537,13 +501,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService { this.logger.error( `Error sending sms about submitApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts index ea24278392a1..7bdc09ff081f 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts @@ -253,16 +253,8 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about initReview in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about initReview in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -279,13 +271,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { this.logger.error( `Error sending sms about initReview to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -327,16 +313,8 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about rejectApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about rejectApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -357,13 +335,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { this.logger.error( `Error sending sms about rejectApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -461,16 +433,8 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about submitApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about submitApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -487,13 +451,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService { this.logger.error( `Error sending sms about submitApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts index eaaae0abe751..4d265b85aebd 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts @@ -283,16 +283,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about initReview in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about initReview in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -309,13 +301,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { this.logger.error( `Error sending sms about initReview to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -423,16 +409,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about addReview in application: ID: ${ - application.id - }, - role: ${ - newlyAddedRecipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === newlyAddedRecipientList[i].ssn, - )}` - }`, + `Error sending email about addReview in application: ID: ${application.id}, + role: ${newlyAddedRecipientList[i].role}`, e, ) }) @@ -452,13 +430,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { this.logger.error( `Error sending sms about addReview to a phonenumber in application: ID: ${application.id}, - role: ${ - newlyAddedRecipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === newlyAddedRecipientList[i].ssn, - )}` - }`, + role: ${newlyAddedRecipientList[i].role}`, e, ) }) @@ -498,16 +470,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about rejectApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about rejectApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -528,13 +492,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { this.logger.error( `Error sending sms about rejectApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) @@ -641,16 +599,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { ) .catch((e) => { this.logger.error( - `Error sending email about submitApplication in application: ID: ${ - application.id - }, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + `Error sending email about submitApplication in application: ID: ${application.id}, + role: ${recipientList[i].role}`, e, ) }) @@ -667,13 +617,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService { this.logger.error( `Error sending sms about rejectApplication to a phonenumber in application: ID: ${application.id}, - role: ${ - recipientList[i].ssn === application.applicant - ? 'Applicant' - : `Assignee index ${application.assignees.findIndex( - (assignee) => assignee === recipientList[i].ssn, - )}` - }`, + role: ${recipientList[i].role}`, e, ) }) From c3b59aac5837052c5f0b85355a6b6bff17f7250f Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:36:07 +0000 Subject: [PATCH 147/173] fix(portals-admin): Parliamentary collection (#16142) * test * merge * merge tweaks * aftermerge-tweaks * tw * download reports * key * tweaks * format --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../signature-collection/src/lib/messages.ts | 123 +++++++++++- .../src/lib/navigation.ts | 2 +- .../Constituency/index.tsx | 68 ++++++- .../DownloadReports/index.tsx | 62 ++++++ .../src/screens-parliamentary/List/index.tsx | 29 ++- .../identityAndCanSignLookup.graphql | 11 + .../List/paperSignees/index.tsx | 188 ++++++++++++++++++ .../paperSignees/uploadPaperSignee.graphql | 8 + .../src/screens-parliamentary/index.tsx | 38 +++- .../screens-presidential/AllLists/index.tsx | 2 +- .../shared-components/signees/editPage.tsx | 90 +++++++++ .../src/shared-components/signees/index.tsx | 20 +- .../ViewList/Signees/PaperSignees.tsx | 2 + .../OwnerView/ViewList/Signees/index.tsx | 14 +- 14 files changed, 625 insertions(+), 32 deletions(-) create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx create mode 100644 libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql create mode 100644 libs/portals/admin/signature-collection/src/shared-components/signees/editPage.tsx diff --git a/libs/portals/admin/signature-collection/src/lib/messages.ts b/libs/portals/admin/signature-collection/src/lib/messages.ts index a74985c729d8..2d997e5699ee 100644 --- a/libs/portals/admin/signature-collection/src/lib/messages.ts +++ b/libs/portals/admin/signature-collection/src/lib/messages.ts @@ -9,6 +9,11 @@ export const m = defineMessages({ }, signatureListsTitle: { id: 'admin-portal.signature-collection:signatureLists', + defaultMessage: 'Meðmælasafnanir', + description: '', + }, + signatureListsTitlePresidential: { + id: 'admin-portal.signature-collection-parliamentary:signatureLists', defaultMessage: 'Forsetakosningar', description: '', }, @@ -187,6 +192,38 @@ export const m = defineMessages({ description: '', }, + /* Hætta við söfnun modal */ + cancelCollectionButton: { + id: 'dmin-portal.signature-collection:cancelCollectionButton', + defaultMessage: 'Hætta við söfnun meðmæla', + description: '', + }, + cancelCollectionModalMessage: { + id: 'dmin-portal.signature-collection:cancelCollectionModalMessage', + defaultMessage: 'Þú ert að fara að hætta við söfnun meðmæla. Ertu viss?', + description: '', + }, + cancelCollectionModalConfirmButton: { + id: 'dmin-portal.signature-collection:modalConfirmButton', + defaultMessage: 'Já, hætta við', + description: '', + }, + cancelCollectionModalCancelButton: { + id: 'dmin-portal.signature-collection:cancelCollectionModalCancelButton', + defaultMessage: 'Nei, hætta við', + description: '', + }, + cancelCollectionModalToastError: { + id: 'dmin-portal.signature-collection:modalToastError', + defaultMessage: 'Ekki tókst að hætta við söfnun meðmæla', + description: '', + }, + cancelCollectionModalToastSuccess: { + id: 'dmin-portal.signature-collection:cancelCollectionModalToastSuccess', + defaultMessage: 'Tókst að hætta við söfnun meðmæla', + description: '', + }, + // View list singleList: { id: 'admin-portal.signature-collection:singleList', @@ -230,6 +267,22 @@ export const m = defineMessages({ defaultMessage: 'Yfirlit meðmæla', description: '', }, + downloadReports: { + id: 'admin-portal.signature-collection:downloadReports', + defaultMessage: 'Sækja skýrslur', + description: '', + }, + downloadReportsDescription: { + id: 'admin-portal.signature-collection:downloadReportsDescription', + defaultMessage: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec odio ultricies ultricies. Nullam nec purus nec odio ultricies ultricies.', + description: '', + }, + downloadButton: { + id: 'admin-portal.signature-collection:downloadButton', + defaultMessage: 'Hlaða niður', + description: '', + }, searchInListPlaceholder: { id: 'admin-portal.signature-collection:searchInListPlaceholder', defaultMessage: 'Leitaðu að nafni eða kennitölu', @@ -490,14 +543,74 @@ export const m = defineMessages({ defaultMessage: 'Loka lista', description: '', }, + paperSigneesHeader: { + id: 'admin-portal.signature-collection:paperSigneesHeader', + defaultMessage: 'Skrá meðmæli af blaði', + description: '', + }, + paperSigneesClearButton: { + id: 'admin-portal.signature-collection:paperSigneesClearButton', + defaultMessage: 'Hreinsa', + description: '', + }, + paperNumber: { + id: 'admin-portal.signature-collection:paperNumber', + defaultMessage: 'Blaðsíðunúmer', + description: '', + }, + editPaperNumber: { + id: 'admin-portal.signature-collection:editPaperNumber', + defaultMessage: 'Breyta blaðsíðunúmeri', + description: '', + }, + saveEditPaperNumber: { + id: 'admin-portal.signature-collection:saveEditPaperNumber', + defaultMessage: 'Uppfæra blaðsíðunúmer', + description: '', + }, + paperSigneeName: { + id: 'admin-portal.signature-collection:paperSigneeName', + defaultMessage: 'Nafn meðmælanda', + description: '', + }, + signPaperSigneeButton: { + id: 'admin-portal.signature-collection:signPaperSigneeButton', + defaultMessage: 'Skrá meðmæli á lista', + description: '', + }, + paperSigneeTypoTitle: { + id: 'admin-portal.signature-collection:paperSigneeTypoTitle', + defaultMessage: 'Kennitala ekki á réttu formi', + description: '', + }, + paperSigneeTypoMessage: { + id: 'admin-portal.signature-collection:paperSigneeTypoMessage', + defaultMessage: 'Vinsamlegast athugið kennitöluna og reynið aftur', + description: '', + }, + paperSigneeCantSignTitle: { + id: 'admin-portal.signature-collection:paperSigneeCantSignTitle', + defaultMessage: 'Ekki er hægt að skrá meðmæli', + description: '', + }, + paperSigneeCantSignMessage: { + id: 'admin-portal.signature-collection:paperSigneeCantSign', + defaultMessage: 'Kennitala uppfyllir ekki skilyrði fyrir að skrá meðmæli', + description: '', + }, + paperSigneeSuccess: { + id: 'admin-portal.signature-collection:paperSigneeSuccess', + defaultMessage: 'Meðmæli skráð', + description: '', + }, + paperSigneeError: { + id: 'admin-portal.signature-collection:paperSigneeError', + defaultMessage: 'Ekki tókst að skrá meðmæli', + description: '', + }, }) export const parliamentaryMessages = defineMessages({ - listTitle: { - id: 'admin-portal.signature-collection-parliamentary:listTitle', - defaultMessage: 'Alþingiskosningar', - description: '', - }, signatureListsTitle: { id: 'admin-portal.signature-collection-parliamentary:signatureLists', defaultMessage: 'Alþingiskosningar', diff --git a/libs/portals/admin/signature-collection/src/lib/navigation.ts b/libs/portals/admin/signature-collection/src/lib/navigation.ts index 62626e9198c8..c48d371f7453 100644 --- a/libs/portals/admin/signature-collection/src/lib/navigation.ts +++ b/libs/portals/admin/signature-collection/src/lib/navigation.ts @@ -11,7 +11,7 @@ export const signatureCollectionNavigation: PortalNavigationItem = { path: SignatureCollectionPaths.ParliamentaryRoot, children: [ { - name: parliamentaryMessages.listTitle, + name: parliamentaryMessages.signatureListsTitle, path: SignatureCollectionPaths.ParliamentaryRoot, activeIfExact: true, }, diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx index 03c72524c7a2..179f5c2d78fe 100644 --- a/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx @@ -5,6 +5,10 @@ import { m, parliamentaryMessages } from '../../lib/messages' import { ActionCard, Box, + Breadcrumbs, + DialogPrompt, + Icon, + Tag, GridColumn, GridContainer, GridRow, @@ -44,6 +48,23 @@ export const Constituency = () => { offset={['0', '0', '0', '1/12']} span={['12/12', '12/12', '12/12', '8/12']} > + + + { {constituencyLists.map((list) => ( { ) }, }} + tag={{ + label: 'Cancel collection', + renderTag: () => ( + + + + + + } + onConfirm={() => { + //onCancelCollection(list.id) + }} + buttonTextConfirm={formatMessage( + m.cancelCollectionModalConfirmButton, + )} + buttonPropsConfirm={{ + variant: 'primary', + colorScheme: 'destructive', + }} + buttonTextCancel={formatMessage( + m.cancelCollectionModalCancelButton, + )} + /> + ), + }} /> ))} diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx new file mode 100644 index 000000000000..d01d8442eb30 --- /dev/null +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx @@ -0,0 +1,62 @@ +import { useLocale } from '@island.is/localization' +import { ActionCard, Box, Button, Stack, Text } from '@island.is/island-ui/core' +import { useState } from 'react' +import { Modal } from '@island.is/react/components' +import { SignatureCollectionArea } from '@island.is/api/schema' +import { m } from '../../lib/messages' + +export const DownloadReports = ({ + areas, +}: { + areas: SignatureCollectionArea[] +}) => { + const { formatMessage } = useLocale() + const [modalDownloadReportsIsOpen, setModalDownloadReportsIsOpen] = + useState(false) + + return ( + + + setModalDownloadReportsIsOpen(false)} + closeButtonLabel={''} + > + {formatMessage(m.downloadReportsDescription)} + + + {areas.map((area) => ( + { + console.log('download') + }, + }} + /> + ))} + + + + + ) +} + +export default DownloadReports diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx index 9f0cca263f80..1f40fa8e7ed7 100644 --- a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx @@ -1,10 +1,18 @@ -import { GridColumn, GridContainer, GridRow } from '@island.is/island-ui/core' +import { + Box, + Breadcrumbs, + GridColumn, + GridContainer, + GridRow, +} from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { IntroHeader, PortalNavigation } from '@island.is/portals/core' import { signatureCollectionNavigation } from '../../lib/navigation' import { m, parliamentaryMessages } from '../../lib/messages' import { useLoaderData } from 'react-router-dom' import { SignatureCollectionList } from '@island.is/api/schema' +import { PaperSignees } from './paperSignees' +import { SignatureCollectionPaths } from '../../lib/paths' import ActionExtendDeadline from '../../shared-components/extendDeadline' import Signees from '../../shared-components/signees' import ActionReviewComplete from '../../shared-components/completeReview' @@ -32,6 +40,24 @@ const List = () => { offset={['0', '0', '0', '1/12']} span={['12/12', '12/12', '12/12', '8/12']} > + + + { /> + diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql new file mode 100644 index 000000000000..373c3e542c71 --- /dev/null +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql @@ -0,0 +1,11 @@ +query Identity($input: IdentityInput!) { + identity(input: $input) { + nationalId + type + name + } +} + +query CanSign($input: SignatureCollectionCanSignFromPaperInput!) { + signatureCollectionCanSignFromPaper(input: $input) +} diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx new file mode 100644 index 000000000000..083cb10e2b7b --- /dev/null +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx @@ -0,0 +1,188 @@ +import { + Box, + Text, + Button, + GridRow, + GridColumn, + GridContainer, + AlertMessage, + Input, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import * as nationalId from 'kennitala' +import { useEffect, useState } from 'react' +import { InputController } from '@island.is/shared/form-fields' +import { useForm } from 'react-hook-form' +import { toast } from 'react-toastify' +import { m } from '../../../lib/messages' +import { + useCanSignQuery, + useIdentityQuery, +} from './identityAndCanSignLookup.generated' +import { useSignatureCollectionUploadPaperSignatureMutation } from './uploadPaperSignee.generated' + +export const PaperSignees = ({ listId }: { listId: string }) => { + useNamespaces('sp.signatureCollection') + const { formatMessage } = useLocale() + const { control, reset } = useForm() + + const [nationalIdInput, setNationalIdInput] = useState('') + const [nationalIdTypo, setNationalIdTypo] = useState(false) + const [page, setPage] = useState('') + const [name, setName] = useState('') + + const { data, loading } = useIdentityQuery({ + variables: { input: { nationalId: nationalIdInput } }, + skip: nationalIdInput.length !== 10 || !nationalId.isValid(nationalIdInput), + onCompleted: (data) => setName(data.identity?.name || ''), + }) + + const { data: canSign, loading: loadingCanSign } = useCanSignQuery({ + variables: { + input: { + signeeNationalId: nationalIdInput, + listId, + }, + }, + }) + + useEffect(() => { + if (nationalIdInput.length === 10) { + setNationalIdTypo( + !nationalId.isValid(nationalIdInput) || + (!loading && !data?.identity?.name), + ) + } else { + setName('') + setNationalIdTypo(false) + } + }, [nationalIdInput, loading, data]) + + const [uploadPaperSignee, { loading: uploadingPaperSignature }] = + useSignatureCollectionUploadPaperSignatureMutation({ + variables: { + input: { + listId: listId, + nationalId: nationalIdInput, + pageNumber: Number(page), + }, + }, + onCompleted: () => { + toast.success(formatMessage(m.paperSigneeSuccess)) + reset() + setNationalIdTypo(false) + setName('') + }, + onError: () => { + toast.error(formatMessage(m.paperSigneeError)) + }, + }) + + const onClearForm = () => { + reset() // resets nationalId field + setNationalIdTypo(false) + setName('') + } + + return ( + + + + {formatMessage(m.paperSigneesHeader)} + + + + + + + + + + + { + setNationalIdInput(e.target.value.replace(/\W/g, '')) + }} + error={nationalIdTypo ? ' ' : undefined} + loading={loading || loadingCanSign} + icon={name && canSign ? 'checkmark' : undefined} + /> + + + setPage(e.target.value)} + /> + + + + + + + + + + + + + {nationalIdTypo && ( + + + + )} + {name && !loadingCanSign && !canSign && ( + + + + )} + + ) +} diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql new file mode 100644 index 000000000000..d06c5c01131f --- /dev/null +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql @@ -0,0 +1,8 @@ +mutation SignatureCollectionUploadPaperSignature( + $input: SignatureCollectionUploadPaperSignatureInput! +) { + signatureCollectionUploadPaperSignature(input: $input) { + success + reasons + } +} diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx index 54b6428860c7..b9abe304a76f 100644 --- a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx @@ -6,6 +6,7 @@ import { GridRow, Stack, Box, + Breadcrumbs, Text, } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' @@ -16,6 +17,7 @@ import { useLoaderData, useNavigate } from 'react-router-dom' import { SignatureCollectionPaths } from '../lib/paths' import CompareLists from '../shared-components/compareLists' import { ListsLoaderReturn } from '../loaders/AllLists.loader' +import DownloadReports from './DownloadReports' const ParliamentaryRoot = () => { const { formatMessage } = useLocale() @@ -40,23 +42,41 @@ const ParliamentaryRoot = () => { offset={['0', '0', '0', '1/12']} span={['12/12', '12/12', '12/12', '8/12']} > + + + - - console.log('search')} - placeholder={formatMessage(m.searchInListPlaceholder)} - backgroundColor="blue" - /> + + + console.log('search')} + placeholder={formatMessage(m.searchInListPlaceholder)} + backgroundColor="blue" + /> + + - {formatMessage(m.totalListResults) + ' ' + collection?.areas.length} + {formatMessage(m.totalListResults)}: {collection?.areas.length} {collection?.areas.map((area) => ( diff --git a/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx index b10fd2ce50a5..cdf864934e62 100644 --- a/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx @@ -125,7 +125,7 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => { span={['12/12', '12/12', '12/12', '8/12']} > { + const { formatMessage } = useLocale() + const [newPage, setNewPage] = useState(page) + const [modalIsOpen, setModalIsOpen] = useState(false) + + return ( + + setModalIsOpen(true)} cursor="pointer"> + + + { + setNewPage(page) + setModalIsOpen(false) + }} + hideOnClickOutside={false} + closeButtonLabel={''} + label={''} + > + + + + + setNewPage(Number(e.target.value))} + backgroundColor="blue" + /> + + + + + + + + + + + + ) +} + +export default EditPage diff --git a/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx b/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx index 1702286ed8cd..a29b504a2bcf 100644 --- a/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx +++ b/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx @@ -17,6 +17,7 @@ import { SignatureCollectionSignature as Signature } from '@island.is/api/schema import SortSignees from '../sortSignees' import { pageSize } from '../../lib/utils' import { m } from '../../lib/messages' +import EditPage from './editPage' const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { const { formatMessage } = useLocale() @@ -54,7 +55,7 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { value={searchTerm} onChange={(v) => setSearchTerm(v)} placeholder={formatMessage(m.searchInListPlaceholder)} - backgroundColor="white" + backgroundColor="blue" /> @@ -116,13 +117,16 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { {!s.isDigital && ( {s.pageNumber} - - - + + )} diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx index 5519f717939d..6589d6d8ce3b 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx @@ -121,6 +121,7 @@ export const PaperSignees = ({ name="nationalId" label={formatMessage(m.signeeNationalId)} format="######-####" + required defaultValue={nationalIdInput} onChange={(e) => { setNationalIdInput(e.target.value.replace(/\W/g, '')) @@ -135,6 +136,7 @@ export const PaperSignees = ({ id="page" name="page" type="number" + required label={formatMessage(m.paperNumber)} value={page} onChange={(e) => setPage(e.target.value)} diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx index 581ffb77b6be..5b5aebf5875b 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx @@ -98,13 +98,15 @@ const Signees = () => { {!s.isDigital && ( - - + {s.pageNumber} + + + )} From 102d8db60bbb4683ea9c5b0978c37bc79f326a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20Gu=C3=B0mundsson?= <34029342+GunnlaugurG@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:04:50 +0000 Subject: [PATCH 148/173] feat(auth-api): Zendesk credentials (#16162) * fix zendesk credentials * chore: charts update dirty files --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/services/auth/admin-api/infra/auth-admin-api.ts | 7 +++++++ charts/identity-server/values.dev.yaml | 3 +++ charts/identity-server/values.prod.yaml | 3 +++ charts/identity-server/values.staging.yaml | 3 +++ 4 files changed, 16 insertions(+) diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index 8e9d21d11f06..fff13b8131cc 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -65,8 +65,15 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { prod: 'https://api.syslumenn.is', }, SYSLUMENN_TIMEOUT: '3000', + ZENDESK_CONTACT_FORM_SUBDOMAIN: { + prod: 'digitaliceland', + staging: 'digitaliceland', + dev: 'digitaliceland', + }, }) .secrets({ + ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL', + ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN', CLIENT_SECRET_ENCRYPTION_KEY: '/k8s/services-auth/admin-api/CLIENT_SECRET_ENCRYPTION_KEY', IDENTITY_SERVER_CLIENT_SECRET: diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index d654243b8f1c..5f76287c7ee4 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -238,6 +238,7 @@ services-auth-admin-api: XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV' + ZENDESK_CONTACT_FORM_SUBDOMAIN: 'digitaliceland' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' @@ -295,6 +296,8 @@ services-auth-admin-api: NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' + ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL' + ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 1e365eced0e1..1c3c4d8a443b 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -235,6 +235,7 @@ services-auth-admin-api: XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' XROAD_TLS_BASE_PATH: 'https://securityserver.island.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS' + ZENDESK_CONTACT_FORM_SUBDOMAIN: 'digitaliceland' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' @@ -292,6 +293,8 @@ services-auth-admin-api: NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' + ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL' + ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index 3c2233c431e0..bee228ee9292 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -238,6 +238,7 @@ services-auth-admin-api: XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST' + ZENDESK_CONTACT_FORM_SUBDOMAIN: 'digitaliceland' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' @@ -295,6 +296,8 @@ services-auth-admin-api: NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD' SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME' + ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL' + ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN' securityContext: allowPrivilegeEscalation: false privileged: false From 94ab9ecfc8ca6052ab63044d3de1e125ae5dc096 Mon Sep 17 00:00:00 2001 From: kksteini <77672665+kksteini@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:29:01 +0000 Subject: [PATCH 149/173] feat(signature-collection-api-domains): Granular access (#16086) * feat(signature-collection-api-domains): Granular access * Updated indexes for modules, added tests to decorators and fixed * Add more tests * Fix tests * Document roles, add decorators and more tests * Remove logs * Simplify tests * Small fix --- .../decorators/acessRequirement.decorator.ts | 11 - .../decorators/allowDelegation.decorator.ts | 4 + .../src/lib/decorators/index.ts | 16 + .../src/lib/decorators/isOwner.decorator.ts | 4 + .../parliamentaryUserTypes.decorator.ts | 28 + .../signature-collection/src/lib/dto/index.ts | 35 ++ .../src/lib/guards/constants.ts | 3 + .../src/lib/guards/userAccess.guard.spec.ts | 489 ++++++++++++++++++ .../src/lib/guards/userAccess.guard.ts | 108 ++-- .../src/lib/models/index.ts | 59 +++ .../src/lib/signatureCollection.resolver.ts | 69 +-- .../src/lib/utils/MetadataAbstractor.ts | 17 + .../src/lib/utils/index.ts | 2 + .../signature-collection/tsconfig.spec.json | 5 +- 14 files changed, 768 insertions(+), 82 deletions(-) delete mode 100644 libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts create mode 100644 libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts create mode 100644 libs/api/domains/signature-collection/src/lib/decorators/index.ts create mode 100644 libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts create mode 100644 libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts create mode 100644 libs/api/domains/signature-collection/src/lib/dto/index.ts create mode 100644 libs/api/domains/signature-collection/src/lib/guards/constants.ts create mode 100644 libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts create mode 100644 libs/api/domains/signature-collection/src/lib/models/index.ts create mode 100644 libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts create mode 100644 libs/api/domains/signature-collection/src/lib/utils/index.ts diff --git a/libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts deleted file mode 100644 index 7960279d7b76..000000000000 --- a/libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { SetMetadata } from '@nestjs/common' - -export enum OwnerAccess { - AllowActor = 'AllowActor', - RestrictActor = 'RestrictActor', -} -export enum UserAccess { - RestrictActor = 'RestrictActor', -} -export const AccessRequirement = (access?: OwnerAccess | UserAccess) => - SetMetadata('owner-access', access) diff --git a/libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts new file mode 100644 index 000000000000..c4256e4aba75 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common' +import { ALLOW_DELEGATION_KEY } from '../guards/constants' + +export const AllowDelegation = () => SetMetadata(ALLOW_DELEGATION_KEY, true) diff --git a/libs/api/domains/signature-collection/src/lib/decorators/index.ts b/libs/api/domains/signature-collection/src/lib/decorators/index.ts new file mode 100644 index 000000000000..9d4ea4508ebd --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/decorators/index.ts @@ -0,0 +1,16 @@ +import { IsOwner } from './isOwner.decorator' +import { CurrentSignee, getCurrentSignee } from './signee.decorator' +import { AllowDelegation } from './allowDelegation.decorator' +import { + AllowManager, + RestrictGuarantor, +} from './parliamentaryUserTypes.decorator' + +export { + AllowDelegation, + CurrentSignee, + IsOwner, + getCurrentSignee, + AllowManager, + RestrictGuarantor, +} diff --git a/libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts new file mode 100644 index 000000000000..4d62d9f5acc2 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common' +import { IS_OWNER_KEY } from '../guards/constants' + +export const IsOwner = () => SetMetadata(IS_OWNER_KEY, true) diff --git a/libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts new file mode 100644 index 000000000000..299721b492ef --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts @@ -0,0 +1,28 @@ +import { SetMetadata } from '@nestjs/common' +import { + ALLOW_DELEGATION_KEY, + RESTRICT_GUARANTOR_KEY, +} from '../guards/constants' +// --------------- +// ---Guarantor--- +// --------------- +// A guarantor is a user in the signature collection system, aimed at parliamentary collections. +// A guarantor (is: Ábyrgðaraðili) defined by Þjóðskrá Íslands as one of the following : +// - A holder of procuration +// OR - A direct candidate in the party ballot + +export const RestrictGuarantor = () => SetMetadata(RESTRICT_GUARANTOR_KEY, true) + +// --------------- +// ----Manager---- +// --------------- +// A manager is a user in the signature collection system, aimed at parliamentary collections. +// A manager (is: Umsjónaraðili) defined by Þjóðskrá Íslands as one of the following: +// - Individuals delegated to a company without having a procuration role +// OR - Individuals delegated to a person (possibly a list owner) + +// This is the same as the allow_delegation rule so no new constants are needed +export const AllowManager = () => SetMetadata(ALLOW_DELEGATION_KEY, true) + +// Assumptions: Guarantors have access to everything unless otherwise stated +// Managers have access to nothing unless otherwise stated diff --git a/libs/api/domains/signature-collection/src/lib/dto/index.ts b/libs/api/domains/signature-collection/src/lib/dto/index.ts new file mode 100644 index 000000000000..a98035626847 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/dto/index.ts @@ -0,0 +1,35 @@ +import { SignatureCollectionAddListsInput } from './addLists.input' +import { SignatureCollectionAreaInput } from './area.input' +import { + BulkUploadUser, + SignatureCollectionListBulkUploadInput, +} from './bulkUpload.input' +import { SignatureCollectionCandidateIdInput } from './candidateId.input' +import { SignatureCollectionCancelListsInput } from './cencelLists.input' +import { SignatureCollectionIdInput } from './collectionId.input' +import { SignatureCollectionExtendDeadlineInput } from './extendDeadline.input' +import { SignatureCollectionListIdInput } from './listId.input' +import { SignatureCollectionNationalIdInput } from './nationalId.input' +import { SignatureCollectionOwnerInput } from './owner.input' +import { SignatureCollectionSignatureIdInput } from './signatureId.input' +import { SignatureCollectionListInput } from './singatureList.input' +import { SignatureCollectionUploadPaperSignatureInput } from './uploadPaperSignature.input' +import { SignatureCollectionCanSignFromPaperInput } from './canSignFromPaper.input' + +export { + SignatureCollectionAddListsInput, + SignatureCollectionAreaInput, + SignatureCollectionListBulkUploadInput, + BulkUploadUser, + SignatureCollectionCandidateIdInput, + SignatureCollectionCancelListsInput, + SignatureCollectionIdInput, + SignatureCollectionExtendDeadlineInput, + SignatureCollectionListIdInput, + SignatureCollectionNationalIdInput, + SignatureCollectionOwnerInput, + SignatureCollectionSignatureIdInput, + SignatureCollectionListInput, + SignatureCollectionUploadPaperSignatureInput, + SignatureCollectionCanSignFromPaperInput, +} diff --git a/libs/api/domains/signature-collection/src/lib/guards/constants.ts b/libs/api/domains/signature-collection/src/lib/guards/constants.ts new file mode 100644 index 000000000000..063d335ecd3d --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/guards/constants.ts @@ -0,0 +1,3 @@ +export const IS_OWNER_KEY = 'is-owner' +export const ALLOW_DELEGATION_KEY = 'allow-delegation' +export const RESTRICT_GUARANTOR_KEY = 'restrict-guarantor' diff --git a/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts new file mode 100644 index 000000000000..b8aa3fc7a80c --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts @@ -0,0 +1,489 @@ +import { Resolver, Query, GraphQLModule } from '@nestjs/graphql' +import { UserAccessGuard } from './userAccess.guard' +import { INestApplication, UseGuards } from '@nestjs/common' +import { + AllowDelegation, + IsOwner, + RestrictGuarantor, + AllowManager, +} from '../decorators' +import { Test } from '@nestjs/testing' +import { ApolloDriver } from '@nestjs/apollo' +import { ConfigModule } from '@nestjs/config' +jest.mock('@island.is/auth-nest-tools', () => { + const original = jest.requireActual('@island.is/auth-nest-tools') + return { + ...original, + getRequest: jest.fn(), + } +}) +import { + getRequest, + IdsUserGuard, + MockAuthGuard, + User, +} from '@island.is/auth-nest-tools' +import { createCurrentUser } from '@island.is/testing/fixtures' +import request from 'supertest' +import { + SignatureCollectionClientConfig, + SignatureCollectionClientModule, +} from '@island.is/clients/signature-collection' +import { SignatureCollectionService } from '../signatureCollection.service' +import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' +import { AuthDelegationType } from '@island.is/shared/types' + +const ownerNationalId = '0101303019' +const ownerCompanyId = '0000000000' +const someNationalId = '0101307789' +const someCompanyId = '0000000001' + +const basicUser = createCurrentUser({ + nationalIdType: 'person', + nationalId: someNationalId, +}) + +const authGuard = new MockAuthGuard(basicUser) +const delegatedUserNotToOwner = createCurrentUser({ + nationalIdType: 'person', + actor: { nationalId: someNationalId }, +}) + +const delegatedUserToOwner = createCurrentUser({ + nationalIdType: 'person', + actor: { nationalId: someNationalId }, + nationalId: ownerNationalId, +}) + +const userIsOwnerNotDelegated = createCurrentUser({ + nationalIdType: 'person', + nationalId: ownerNationalId, +}) + +const userHasProcurationAndIsOwner = createCurrentUser({ + nationalIdType: 'company', + actor: { nationalId: someNationalId }, + nationalId: ownerCompanyId, + delegationType: [AuthDelegationType.ProcurationHolder], +}) + +const userHasProcurationAndIsNotOwner = createCurrentUser({ + nationalIdType: 'company', + actor: { nationalId: someNationalId }, + nationalId: someCompanyId, + delegationType: [AuthDelegationType.ProcurationHolder], +}) + +const userDelegatedToCompanyButNotProcurationHolder = createCurrentUser({ + nationalIdType: 'company', + actor: { nationalId: someNationalId }, + nationalId: someCompanyId, + delegationType: [AuthDelegationType.Custom], +}) + +const okGraphQLResponse = (queryName: string) => ({ + data: { + [queryName]: true, + }, +}) + +const forbiddenGraphqlResponse = (queryName: string) => ({ + data: { + [queryName]: null, + }, + errors: [{ message: 'Forbidden resource' }], +}) + +@UseGuards(UserAccessGuard, IdsUserGuard) +@Resolver() +class TestResolver { + @Query(() => Boolean, { nullable: true }) + @IsOwner() + getIfOwner() { + return true + } + + @Query(() => Boolean, { nullable: true }) + @IsOwner() + @AllowDelegation() + getIfOwnerWithDelegationAllowed() { + return true + } + + @Query(() => Boolean, { nullable: true }) + @AllowDelegation() + getIfAllowedDelegation() { + return true + } + + @Query(() => Boolean, { nullable: true }) + getForAllNonDelegatedUsers() { + return true + } + + @Query(() => Boolean, { nullable: true }) + @RestrictGuarantor() + getIsRestrictedToGuarantors() { + return true + } + + @Query(() => Boolean, { nullable: true }) + @AllowManager() + getIsAllowedForManagers() { + return true + } + + @Query(() => Boolean, { nullable: true }) + @RestrictGuarantor() + @AllowManager() + getIsRestrictedToGuarantorsAndAllowedForManagers() { + return true + } + + @Query(() => Boolean, { nullable: true }) + @IsOwner() + @AllowManager() + getIfOwnerWithAllowManager() { + return true + } +} + +describe('UserAccessGuard', () => { + let app: INestApplication + let signatureCollectionService: SignatureCollectionService + const setupMockForUser = (user: User): void => { + jest.spyOn(authGuard, 'getAuth').mockReturnValue(user) + ;(getRequest as jest.Mock).mockReturnValue({ + user, + }) + } + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [TestResolver, SignatureCollectionService], + imports: [ + GraphQLModule.forRoot({ + autoSchemaFile: true, + driver: ApolloDriver, + path: '/graphql', + }), + ConfigModule.forRoot({ + isGlobal: true, + load: [SignatureCollectionClientConfig, IdsClientConfig, XRoadConfig], + }), + SignatureCollectionClientModule, + ], + }) + .overrideGuard(IdsUserGuard) + .useValue(authGuard) + .compile() + + app = moduleRef.createNestApplication() + signatureCollectionService = app.get( + SignatureCollectionService, + ) + + await app.init() + }) + + beforeEach(() => { + jest + .spyOn(signatureCollectionService, 'signee') + .mockImplementation((user: User, _nationalId?: string) => { + return Promise.resolve({ + canCreate: true, + canSign: true, + isOwner: [ownerNationalId, ownerCompanyId].includes(user.nationalId), + name: 'Test', + nationalId: user.nationalId, + candidate: { + id: '1', + name: 'Test', + nationalId: user.nationalId, + }, + }) + }) + + jest + .spyOn(signatureCollectionService, 'isCollector') + .mockImplementation(() => { + return Promise.resolve(true) + }) + }) + + afterEach(() => { + jest.clearAllMocks() + jest.restoreAllMocks() + }) + + const gqlQuery = (query: string) => + request(app.getHttpServer()).get('/graphql').query({ + query, + }) + + it('Should allow owner to access IsOwner decorated paths', async () => { + setupMockForUser(userIsOwnerNotDelegated) + + const response = await gqlQuery('{ getIfOwner }') + + expect(response.body).toMatchObject(okGraphQLResponse('getIfOwner')) + }) + + it('Should not allow user delegated to owner to access IsOwner decorated paths without AllowDelegation', async () => { + setupMockForUser(delegatedUserToOwner) + + const response = await gqlQuery('{ getIfOwner }') + + expect(response.body).toMatchObject(forbiddenGraphqlResponse('getIfOwner')) + }) + + it('Should not allow basic users to access IsOwner when not owner', async () => { + setupMockForUser(basicUser) + + const response = await gqlQuery('{ getIfOwner }') + + expect(response.body).toMatchObject(forbiddenGraphqlResponse('getIfOwner')) + }) + + it('Where AllowDelegation and IsOwner: Should allow user delegated to owner', async () => { + setupMockForUser(delegatedUserToOwner) + + const response = await gqlQuery('{ getIfOwnerWithDelegationAllowed }') + + expect(response.body).toMatchObject( + okGraphQLResponse('getIfOwnerWithDelegationAllowed'), + ) + }) + + it('Where AllowDelegation and IsOwner: Should not allow user delegated to non-owner', async () => { + setupMockForUser(delegatedUserNotToOwner) + + const response = await gqlQuery('{ getIfOwnerWithDelegationAllowed }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getIfOwnerWithDelegationAllowed'), + ) + }) + + it('With no decorators present: Should restrict delegation to owner', async () => { + setupMockForUser(delegatedUserToOwner) + + const response = await gqlQuery('{ getForAllNonDelegatedUsers }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getForAllNonDelegatedUsers'), + ) + }) + + it('With no decorators present: Should restrict delegation to non-owner', async () => { + setupMockForUser(delegatedUserNotToOwner) + + const response = await gqlQuery('{ getForAllNonDelegatedUsers }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getForAllNonDelegatedUsers'), + ) + }) + + it('With no decorators present: Should allow basic users', async () => { + setupMockForUser(basicUser) + + const response = await gqlQuery('{ getForAllNonDelegatedUsers }') + + expect(response.body).toMatchObject( + okGraphQLResponse('getForAllNonDelegatedUsers'), + ) + }) + + it('When IsOwner not present: Should allow delegation with AllowDelegation for owner delegations', async () => { + setupMockForUser(delegatedUserToOwner) + + const response = await gqlQuery('{ getIfAllowedDelegation }') + + expect(response.body).toMatchObject( + okGraphQLResponse('getIfAllowedDelegation'), + ) + }) + + it('When IsOwner not present: Should allow delegation with AllowDelegation for non-owner delegations', async () => { + setupMockForUser(delegatedUserNotToOwner) + + const response = await gqlQuery('{ getIfAllowedDelegation }') + + expect(response.body).toMatchObject( + okGraphQLResponse('getIfAllowedDelegation'), + ) + }) + it('With only AllowDelegation: Should not restrict basic users', async () => { + setupMockForUser(basicUser) + + const response = await gqlQuery('{ getIfAllowedDelegation }') + + expect(response.body).toMatchObject( + okGraphQLResponse('getIfAllowedDelegation'), + ) + }) + + it('With only IsOwner: Should not restrict delegation of a procuration type even with no AllowDelegation when delegated to owner', async () => { + setupMockForUser(userHasProcurationAndIsOwner) + + const response = await gqlQuery('{ getIfOwner }') + + expect(response.body).toMatchObject(okGraphQLResponse('getIfOwner')) + }) + + it('With only IsOwner: Should restrict delegation of a procuration type even with no AllowDelegation when delegated to non-owner', async () => { + setupMockForUser(userHasProcurationAndIsNotOwner) + + const response = await gqlQuery('{ getIfOwner }') + + expect(response.body).toMatchObject(forbiddenGraphqlResponse('getIfOwner')) + }) + + it('With RestrictGuarantor: Should restrict access to guarantors', async () => { + setupMockForUser(userIsOwnerNotDelegated) + + let response = await gqlQuery('{ getIsRestrictedToGuarantors }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getIsRestrictedToGuarantors'), + ) + + setupMockForUser(userHasProcurationAndIsOwner) + + response = await gqlQuery('{ getIsRestrictedToGuarantors }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getIsRestrictedToGuarantors'), + ) + + setupMockForUser(userHasProcurationAndIsNotOwner) + + response = await gqlQuery('{ getIsRestrictedToGuarantors }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getIsRestrictedToGuarantors'), + ) + }) + + it('With RestrictGuarantor and AllowManager: Should allow access to managers', async () => { + setupMockForUser(userIsOwnerNotDelegated) + + // DISALLOW ALL GUARANTORS + let response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse( + 'getIsRestrictedToGuarantorsAndAllowedForManagers', + ), + ) + + setupMockForUser(userHasProcurationAndIsOwner) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse( + 'getIsRestrictedToGuarantorsAndAllowedForManagers', + ), + ) + + setupMockForUser(userHasProcurationAndIsNotOwner) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse( + 'getIsRestrictedToGuarantorsAndAllowedForManagers', + ), + ) + + // ALLOW ALL MANAGERS + setupMockForUser(delegatedUserNotToOwner) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'), + ) + + setupMockForUser(delegatedUserToOwner) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'), + ) + + setupMockForUser(userDelegatedToCompanyButNotProcurationHolder) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'), + ) + }) + + it('With AllowManager: Should allow access to managers', async () => { + // ALLOW ALL MANAGERS + setupMockForUser(delegatedUserNotToOwner) + + let response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'), + ) + + setupMockForUser(delegatedUserToOwner) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'), + ) + + setupMockForUser(userDelegatedToCompanyButNotProcurationHolder) + + response = await gqlQuery( + '{ getIsRestrictedToGuarantorsAndAllowedForManagers }', + ) + + expect(response.body).toMatchObject( + okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'), + ) + }) + + it('Allow manager does not override the IsOwner decorator', async () => { + setupMockForUser(delegatedUserToOwner) + + let response = await gqlQuery('{ getIfOwnerWithAllowManager }') + + expect(response.body).toMatchObject( + okGraphQLResponse('getIfOwnerWithAllowManager'), + ) + + setupMockForUser(delegatedUserNotToOwner) + + response = await gqlQuery('{ getIfOwnerWithAllowManager }') + + expect(response.body).toMatchObject( + forbiddenGraphqlResponse('getIfOwnerWithAllowManager'), + ) + }) +}) diff --git a/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts index b3fbfa65c404..3f485e4a9528 100644 --- a/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts +++ b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts @@ -1,12 +1,22 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common' import { Reflector } from '@nestjs/core' -import { BYPASS_AUTH_KEY, getRequest } from '@island.is/auth-nest-tools' -import { - OwnerAccess, - UserAccess, -} from '../decorators/acessRequirement.decorator' +import { BYPASS_AUTH_KEY, getRequest, User } from '@island.is/auth-nest-tools' import { SignatureCollectionService } from '../signatureCollection.service' +import { MetadataAbstractor } from '../utils' import { AuthDelegationType } from '@island.is/shared/types' +import { isPerson } from 'kennitala' +import { + ALLOW_DELEGATION_KEY, + IS_OWNER_KEY, + RESTRICT_GUARANTOR_KEY, +} from './constants' + +enum UserDelegationContext { + Person = 'Person', + PersonDelegatedToPerson = 'PersonDelegatedToPerson', + PersonDelegatedToCompany = 'PersonDelegatedToCompany', + ProcurationHolder = 'ProcurationHolder', +} @Injectable() export class UserAccessGuard implements CanActivate { @@ -14,56 +24,82 @@ export class UserAccessGuard implements CanActivate { private reflector: Reflector, private readonly signatureCollectionService: SignatureCollectionService, ) {} + + private determineUserDelegationContext( + user: Express.User & User, + ): UserDelegationContext { + // If actor found on user, then user is delegated + if (user.actor?.nationalId) { + // If delegation is from person to person + if (isPerson(user.nationalId)) { + return UserDelegationContext.PersonDelegatedToPerson + } else { + // Determine whether it's a procuration vs delegation to a company + const hasProcuration = user.delegationType?.some( + (delegation) => delegation === AuthDelegationType.ProcurationHolder, + ) + + return hasProcuration + ? UserDelegationContext.ProcurationHolder + : UserDelegationContext.PersonDelegatedToCompany + } + } + + return UserDelegationContext.Person + } + async canActivate(context: ExecutionContext): Promise { - const bypassAuth = this.reflector.getAllAndOverride( - BYPASS_AUTH_KEY, - [context.getHandler(), context.getClass()], + const m = new MetadataAbstractor(this.reflector, context) + const isOwnerRestriction = m.getMetadataIfExists(IS_OWNER_KEY) + const bypassAuth = m.getMetadataIfExists(BYPASS_AUTH_KEY) + const allowDelegation = m.getMetadataIfExists(ALLOW_DELEGATION_KEY) + const restrictGuarantors = m.getMetadataIfExists( + RESTRICT_GUARANTOR_KEY, ) - // if the bypass auth exists and is truthy we bypass auth if (bypassAuth) { return true } - const ownerRestriction = this.reflector.get( - 'owner-access', - context.getHandler(), - ) - const request = getRequest(context) + const request = getRequest(context) const user = request.user if (!user) { return false } - const isDelegatedUser = !!user?.actor?.nationalId - const isProcurationHolder = user?.delegationType?.some( - (delegation) => delegation === AuthDelegationType.ProcurationHolder, - ) + const delegationContext = this.determineUserDelegationContext(user) + const isDelegatedUser = [ + UserDelegationContext.PersonDelegatedToCompany, + UserDelegationContext.PersonDelegatedToPerson, + ].includes(delegationContext) + + if (isDelegatedUser && !allowDelegation) { + return false + } + + if (restrictGuarantors && !isDelegatedUser) { + return false + } + // IsOwner needs signee const signee = await this.signatureCollectionService.signee(user) request.body = { ...request.body, signee } - // IsOwner decorator not used - if (!ownerRestriction) { - return true - } - if (ownerRestriction === UserAccess.RestrictActor) { - return isDelegatedUser ? false : true - } const { candidate } = signee - if (signee.isOwner && candidate) { - // Check if user is an actor for owner and if so check if registered collector, if not actor will be added as collector - if (isDelegatedUser && ownerRestriction === OwnerAccess.AllowActor) { - const isCollector = await this.signatureCollectionService.isCollector( - candidate.id, - user, - ) - return isCollector + if (isOwnerRestriction) { + if (signee.isOwner && candidate) { + // Check if user is an actor for owner and if so check if registered collector, if not actor will be added as collector + if (isDelegatedUser && allowDelegation) { + const isCollector = await this.signatureCollectionService.isCollector( + candidate.id, + user, + ) + return isCollector + } } - return true + return signee.isOwner } - // if the user is not owner we return false - return false + return true } } diff --git a/libs/api/domains/signature-collection/src/lib/models/index.ts b/libs/api/domains/signature-collection/src/lib/models/index.ts new file mode 100644 index 000000000000..73e6f2d9ab85 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/models/index.ts @@ -0,0 +1,59 @@ +import { + SignatureCollectionArea, + SignatureCollectionAreaBase, +} from './area.model' +import { + SignatureCollectionBulk, + SignatureCollectionNationalIds, +} from './bulk.model' +import { CanSignInfo } from './canSignInfo.model' +import { SignatureCollectionCandidate } from './candidate.model' +import { SignatureCollection } from './collection.model' +import { SignatureCollectionCollector } from './collector.model' +import { SignatureCollectionNationalIdError } from './nationalIdError.model' +import { SignatureCollectionSignature } from './signature.model' +import { + SignatureCollectionList, + SignatureCollectionListBase, + SignatureCollectionOwnedList, + SignatureCollectionSignedList, +} from './signatureList.model' +import { + SignatureCollectionCandidateLookUp, + SignatureCollectionSignee, + SignatureCollectionSigneeBase, +} from './signee.model' +import { SignatureCollectionSlug } from './slug.model' +import { + CollectionStatus, + ListStatus, + SignatureCollectionListStatus, + SignatureCollectionStatus, +} from './status.model' +import { SignatureCollectionSuccess } from './success.model' + +export { + SignatureCollectionArea, + SignatureCollectionAreaBase, + SignatureCollectionBulk, + SignatureCollectionNationalIds, + CanSignInfo, + SignatureCollectionCandidate, + SignatureCollection, + SignatureCollectionCollector, + SignatureCollectionNationalIdError, + SignatureCollectionSignature, + SignatureCollectionList, + SignatureCollectionListBase, + SignatureCollectionOwnedList, + SignatureCollectionSignedList, + SignatureCollectionCandidateLookUp, + SignatureCollectionSignee, + SignatureCollectionSigneeBase, + SignatureCollectionSlug, + CollectionStatus, + ListStatus, + SignatureCollectionListStatus, + SignatureCollectionStatus, + SignatureCollectionSuccess, +} diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts index 4b27ba5a6ec9..88300bbb9c23 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts @@ -10,32 +10,33 @@ import { Scopes, } from '@island.is/auth-nest-tools' import { UseGuards } from '@nestjs/common' -import { SignatureCollection } from './models/collection.model' +import { Audit } from '@island.is/nest/audit' +import { UserAccessGuard } from './guards/userAccess.guard' +import { ApiScope } from '@island.is/auth/scopes' +import { + SignatureCollectionAddListsInput, + SignatureCollectionCancelListsInput, + SignatureCollectionCanSignFromPaperInput, + SignatureCollectionIdInput, + SignatureCollectionListIdInput, + SignatureCollectionUploadPaperSignatureInput, +} from './dto' +import { + AllowDelegation, + AllowManager, + CurrentSignee, + IsOwner, +} from './decorators' import { + SignatureCollection, + SignatureCollectionCollector, SignatureCollectionList, SignatureCollectionListBase, + SignatureCollectionSignature, SignatureCollectionSignedList, -} from './models/signatureList.model' -import { SignatureCollectionListIdInput } from './dto/listId.input' -import { SignatureCollectionSignature } from './models/signature.model' -import { SignatureCollectionSignee } from './models/signee.model' -import { Audit } from '@island.is/nest/audit' -import { UserAccessGuard } from './guards/userAccess.guard' -import { - AccessRequirement, - OwnerAccess, - UserAccess, -} from './decorators/acessRequirement.decorator' -import { CurrentSignee } from './decorators/signee.decorator' -import { ApiScope } from '@island.is/auth/scopes' -import { SignatureCollectionCancelListsInput } from './dto/cencelLists.input' -import { SignatureCollectionIdInput } from './dto/collectionId.input' -import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input' -import { SignatureCollectionAddListsInput } from './dto/addLists.input' -import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input' -import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input' -import { SignatureCollectionCollector } from './models/collector.model' -import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input' + SignatureCollectionSignee, +} from './models' + @UseGuards(IdsUserGuard, ScopesGuard, UserAccessGuard) @Resolver() @Audit({ namespace: '@island.is/api/signature-collection' }) @@ -66,7 +67,8 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.AllowActor) + @AllowManager() + @IsOwner() @Query(() => [SignatureCollectionList]) @Audit() async signatureCollectionListsForOwner( @@ -78,7 +80,6 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(UserAccess.RestrictActor) @Query(() => [SignatureCollectionListBase]) @Audit() async signatureCollectionListsForUser( @@ -90,7 +91,8 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.AllowActor) + @IsOwner() + @AllowManager() @Query(() => SignatureCollectionList) @Audit() async signatureCollectionList( @@ -101,7 +103,6 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(UserAccess.RestrictActor) @Query(() => [SignatureCollectionSignedList], { nullable: true }) @Audit() async signatureCollectionSignedList( @@ -111,7 +112,8 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.AllowActor) + @IsOwner() + @AllowManager() @Query(() => [SignatureCollectionSignature], { nullable: true }) @Audit() async signatureCollectionSignatures( @@ -123,7 +125,6 @@ export class SignatureCollectionResolver { @Scopes(ApiScope.signatureCollection) @Query(() => SignatureCollectionSignee) - @AccessRequirement(UserAccess.RestrictActor) @Audit() async signatureCollectionSignee( @CurrentSignee() signee: SignatureCollectionSignee, @@ -133,7 +134,8 @@ export class SignatureCollectionResolver { @Scopes(ApiScope.signatureCollection) @Query(() => Boolean) - @AccessRequirement(OwnerAccess.AllowActor) + @IsOwner() + @AllowManager() @Audit() async signatureCollectionCanSignFromPaper( @Args('input') input: SignatureCollectionCanSignFromPaperInput, @@ -143,7 +145,6 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(UserAccess.RestrictActor) @Mutation(() => SignatureCollectionSuccess) @Audit() async signatureCollectionUnsign( @@ -154,7 +155,7 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.RestrictActor) + @IsOwner() @Mutation(() => SignatureCollectionSuccess) @Audit() async signatureCollectionCancel( @@ -165,7 +166,7 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.RestrictActor) + @IsOwner() @Mutation(() => SignatureCollectionSuccess) @Audit() async signatureCollectionAddAreas( @@ -176,7 +177,7 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.RestrictActor) + @IsOwner() @Mutation(() => SignatureCollectionSuccess) @Audit() async signatureCollectionUploadPaperSignature( @@ -190,7 +191,7 @@ export class SignatureCollectionResolver { } @Scopes(ApiScope.signatureCollection) - @AccessRequirement(OwnerAccess.AllowActor) + @IsOwner() @Query(() => [SignatureCollectionCollector]) @Audit() async signatureCollectionCollectors( diff --git a/libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts b/libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts new file mode 100644 index 000000000000..92cdcf5089c0 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts @@ -0,0 +1,17 @@ +import { ExecutionContext } from '@nestjs/common' +import { Reflector } from '@nestjs/core' + +export class MetadataAbstractor { + constructor( + private readonly reflector: Reflector, + private readonly context: ExecutionContext, + ) {} + + public getMetadataIfExists = (key: string): T | null => { + const reflectorData = this.reflector.getAllAndOverride(key, [ + this.context.getHandler(), + this.context.getClass(), + ]) + return reflectorData ?? null + } +} diff --git a/libs/api/domains/signature-collection/src/lib/utils/index.ts b/libs/api/domains/signature-collection/src/lib/utils/index.ts new file mode 100644 index 000000000000..af1e408a8ab7 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/utils/index.ts @@ -0,0 +1,2 @@ +import { MetadataAbstractor } from './MetadataAbstractor' +export { MetadataAbstractor } diff --git a/libs/api/domains/signature-collection/tsconfig.spec.json b/libs/api/domains/signature-collection/tsconfig.spec.json index 6668655fc397..f59491cc62a7 100644 --- a/libs/api/domains/signature-collection/tsconfig.spec.json +++ b/libs/api/domains/signature-collection/tsconfig.spec.json @@ -3,7 +3,10 @@ "compilerOptions": { "outDir": "../../../../dist/out-tsc", "module": "commonjs", - "types": ["jest", "node"] + "types": ["jest", "node"], + "noPropertyAccessFromIndexSignature": false, + "noImplicitOverride": false, + "noImplicitReturns": false }, "include": [ "jest.config.ts", From b78609d46d6bfbf2290685cce1f4c250565c3ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorkell=20M=C3=A1ni=20=C3=9Eorkelsson?= Date: Thu, 26 Sep 2024 11:35:39 +0000 Subject: [PATCH 150/173] fix(service-portal-core): Conditial org query in tab navigation (#16166) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../TabNavigation/TabNavigation.tsx | 31 +++++---------- .../TabNavigationInstitutionPanel.tsx | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx diff --git a/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx b/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx index 6a371f092197..a08bfe67c73d 100644 --- a/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx +++ b/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx @@ -16,6 +16,7 @@ import { useWindowSize } from 'react-use' import InstitutionPanel from '../InstitutionPanel/InstitutionPanel' import * as styles from './TabNavigation.css' import { TabBar } from './TabBar' +import { TabNavigationInstitutionPanel } from './TabNavigationInstitutionPanel' interface Props { pathname?: string @@ -50,14 +51,10 @@ export const TabNavigation: React.FC = ({ items, pathname, label }) => { navigate(id) } } - - const { data: organization, loading } = useOrganization( - activePath?.activeChild?.serviceProvider ?? activePath.serviceProvider, - ) - + const serviceProvider = + activePath?.activeChild?.serviceProvider ?? activePath.serviceProvider const descriptionText = activePath.activeChild?.description ?? activePath?.description - const tooltipText = activePath.activeChild?.serviceProviderTooltip ?? activePath.serviceProviderTooltip @@ -157,23 +154,13 @@ export const TabNavigation: React.FC = ({ items, pathname, label }) => { )} - {(activePath.displayServiceProviderLogo || - activePath?.displayServiceProviderLogo) && + {activePath?.displayServiceProviderLogo && + serviceProvider && !isMobile && ( - - {organization?.logo && ( - - )} - + )} diff --git a/libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx b/libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx new file mode 100644 index 000000000000..343849cc1431 --- /dev/null +++ b/libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx @@ -0,0 +1,39 @@ +import { GridColumn } from '@island.is/island-ui/core' +import { useOrganization } from '@island.is/service-portal/graphql' +import InstitutionPanel from '../InstitutionPanel/InstitutionPanel' +import { MessageDescriptor } from 'react-intl' +import { OrganizationSlugType } from '@island.is/shared/constants' +import { useLocale } from '@island.is/localization' +import { useWindowSize } from 'react-use' +import { theme } from '@island.is/island-ui/theme' + +interface Props { + serviceProvider: OrganizationSlugType + tooltipText?: MessageDescriptor +} + +export const TabNavigationInstitutionPanel = ({ + tooltipText, + serviceProvider, +}: Props) => { + const { formatMessage } = useLocale() + const { data: organization, loading } = useOrganization(serviceProvider) + const { width } = useWindowSize() + + const isMobile = width < theme.breakpoints.md + + return ( + + {organization?.logo && ( + + )} + + ) +} From ef69855e77f4576bf50b011e3e9942a73fe79d06 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:57:34 +0000 Subject: [PATCH 151/173] fix(web): signature collection link fix (#16169) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../connected/src/lib/SignatureLists/SignatureLists.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx b/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx index b042eddc70e0..fe0d8a9cb905 100644 --- a/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx +++ b/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx @@ -115,7 +115,11 @@ export const SignatureLists: FC< size: 'small', onClick: () => window.open( - `${window.location.origin}/umsoknir/maela-med-frambodi/?candidate=${candidate.id}`, + `${window.location.origin}/umsoknir/${ + collection.isPresidential + ? 'maela-med-frambodi' + : 'maela-med-althingisframbodi' + }/?candidate=${candidate.id}`, '_blank', ), } From 7e1f9d919010e9a10f58dcf4e54dca7ab61cb82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81sd=C3=ADs=20Erna=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Thu, 26 Sep 2024 12:18:08 +0000 Subject: [PATCH 152/173] fix(service-portal): Add locale to organ donation updates (#16089) * refactor: add locale to update organ donations regs * cleanup * cleanup --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/health-directorate.resolver.ts | 4 +++- .../src/lib/health-directorate.service.ts | 20 ++++++++++++------- .../clients/organ-donation/clientConfig.json | 7 +++++++ .../organ-donation/organDonation.service.ts | 2 ++ .../OrganDonation/OrganDonation.graphql | 10 ++++++++-- .../screens/OrganDonation/OrganDonation.tsx | 14 ++++++------- .../RegistrationForm.tsx | 1 + 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts b/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts index 05f73ec5209d..da24d7f71039 100644 --- a/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts +++ b/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts @@ -60,9 +60,11 @@ export class HealthDirectorateResolver { @Audit() async updateDonorStatus( @Args('input') input: DonorInput, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', @CurrentUser() user: User, ): Promise { - return this.api.updateDonorStatus(user, input) + return this.api.updateDonorStatus(user, input, locale) } /* Vaccinations */ diff --git a/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts b/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts index 4e40a778de6c..391a425c4bb0 100644 --- a/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts +++ b/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts @@ -24,8 +24,6 @@ export class HealthDirectorateService { const lang: organLocale = locale === 'is' ? organLocale.Is : organLocale.En const data: OrganDonorDto | null = await this.organDonationApi.getOrganDonation(auth, lang) - // Fetch organ list to get all names in correct language to sort out the names of the organs the user has limitations for - if (data === null) { return null } @@ -58,11 +56,19 @@ export class HealthDirectorateService { return limitations } - async updateDonorStatus(auth: Auth, input: DonorInput): Promise { - return await this.organDonationApi.updateOrganDonation(auth, { - isDonor: input.isDonor, - exceptions: input.organLimitations ?? [], - }) + async updateDonorStatus( + auth: Auth, + input: DonorInput, + locale: Locale, + ): Promise { + return await this.organDonationApi.updateOrganDonation( + auth, + { + isDonor: input.isDonor, + exceptions: input.organLimitations ?? [], + }, + locale === 'is' ? organLocale.Is : organLocale.En, + ) } /* Vaccinations */ diff --git a/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json b/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json index 3f2b4e84bf09..b83056a0484c 100644 --- a/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json +++ b/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json @@ -75,6 +75,13 @@ "in": "query", "description": "The IP address of the user", "schema": { "type": "string" } + }, + { + "name": "locale", + "required": false, + "in": "query", + "description": "The locale to use for the response", + "schema": { "$ref": "#/components/schemas/Locale" } } ], "requestBody": { diff --git a/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts b/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts index 37ceeb3da061..e1b0b502a496 100644 --- a/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts +++ b/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts @@ -46,11 +46,13 @@ export class HealthDirectorateOrganDonationService { public async updateOrganDonation( auth: Auth, input: UpdateOrganDonorDto, + locale: Locale, ): Promise { await this.organDonationApiWithAuth( auth, ).meDonorStatusControllerUpdateOrganDonorStatus({ updateOrganDonorDto: input, + locale: locale, }) } diff --git a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql index e445153ee094..d120a592b167 100644 --- a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql +++ b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql @@ -33,6 +33,12 @@ query getOrgansList($locale: String) { } } -mutation updateOrganDonationInfo($input: HealthDirectorateOrganDonorInput!) { - healthDirectorateOrganDonationUpdateDonorStatus(input: $input) +mutation updateOrganDonationInfo( + $input: HealthDirectorateOrganDonorInput! + $locale: String +) { + healthDirectorateOrganDonationUpdateDonorStatus( + input: $input + locale: $locale + ) } diff --git a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx index ba3501ea7d22..4f6d6e6ff7d1 100644 --- a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx +++ b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx @@ -30,6 +30,12 @@ const OrganDonation = () => { : formatMessage(m.iAmOrganDonorText) : formatMessage(m.iAmNotOrganDonorText) + const heading = donorStatus?.isDonor + ? donorStatus.limitations?.hasLimitations + ? formatMessage(m.iAmOrganDonorWithExceptions) + : formatMessage(m.iAmOrganDonor) + : formatMessage(m.iAmNotOrganDonor) + return ( { {formatMessage(m.takeOnOrganDonation)} { isDonor: radioValue === OPT_IN || radioValue === OPT_IN_EXCEPTIONS, organLimitations: radioValue === OPT_IN_EXCEPTIONS ? limitations : [], }, + locale: lang, }, }) } From ec2b57ed3f47df6f74cd2e6bb134c2eec6784db9 Mon Sep 17 00:00:00 2001 From: birkirkristmunds <142495885+birkirkristmunds@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:30:04 +0000 Subject: [PATCH 153/173] fix(skilavottord): Error when trying to view and save recycling companies (#16159) - Fix path when trying to view recycling company - Fix error when trying to save recycling company Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../web/screens/RecyclingCompanies/RecyclingCompanies.tsx | 2 +- .../RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx index de4040c67755..89f0aee3bd0f 100644 --- a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx +++ b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx @@ -72,7 +72,7 @@ const RecyclingCompanies: FC> = () => { const handleUpdate = (id: string) => { router.push({ - pathname: BASE_PATH + routes.recyclingCompanies.edit, // without BASE-PATH it changes the whole route, probably some bug + pathname: routes.recyclingCompanies.edit, // with BASE-PATH it duplicates the path query: { id }, }) } diff --git a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx index aa2d7ce91e9b..4d1357720c0f 100644 --- a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx +++ b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx @@ -115,6 +115,9 @@ const RecyclingCompanyUpdate: FC> = () => { } const handleUpdateRecyclingPartner = handleSubmit(async (input) => { + // Not needed to be sent to the backend, causes error if it is sent + delete input.__typename + const { errors } = await updateSkilavottordRecyclingPartner({ variables: { input }, }) From a51375981562baea21b648ce3f42aba4a1ca4e8f Mon Sep 17 00:00:00 2001 From: juni-haukur <158475136+juni-haukur@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:03:47 +0000 Subject: [PATCH 154/173] fix(signature-collection): Parliamentary can unsign invalid while list is active (#16167) * Parliamentary can unsign invalid while list is active * update unsign for parliamentary --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/signature-collection.service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.ts index 2fc5e1b67d4d..0a99dc8bd331 100644 --- a/libs/clients/signature-collection/src/lib/signature-collection.service.ts +++ b/libs/clients/signature-collection/src/lib/signature-collection.service.ts @@ -297,8 +297,11 @@ export class SignatureCollectionClientService { } async unsignList(listId: string, auth: User): Promise { + const { isPresidential } = await this.currentCollection() const { signatures } = await this.getSignee(auth) - const activeSignature = signatures?.find((signature) => signature.valid) + const activeSignature = signatures?.find((signature) => + isPresidential ? signature.valid : signature.listId === listId, + ) if (!signatures || !activeSignature || activeSignature.listId !== listId) { return { success: false, reasons: [ReasonKey.SignatureNotFound] } } @@ -373,6 +376,7 @@ export class SignatureCollectionClientService { const isExtended = list.endTime > endTime const signedThisPeriod = signature.isInitialType === !isExtended const canUnsignDigital = isPresidential ? signature.isDigital : true + const canUnsignInvalid = isPresidential ? signature.valid : true return { signedDate: signature.created, isDigital: signature.isDigital, @@ -380,7 +384,7 @@ export class SignatureCollectionClientService { isValid: signature.valid, canUnsign: canUnsignDigital && - signature.valid && + canUnsignInvalid && list.active && signedThisPeriod, ...list, From 4cb5e52f11324667647d77aa363abc668321c4f3 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:36:05 +0000 Subject: [PATCH 155/173] fix(service-portal): listid unsign (#16172) * fix(service-portal): listid unsign * chore: nx format:write update dirty files * minor tweaks --------- Co-authored-by: andes-it --- .../signature-collection/src/lib/messages.ts | 15 ++-- .../src/screens-parliamentary/index.tsx | 7 +- .../src/screens/shared/SignedList/index.tsx | 80 ++++++++++--------- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/libs/portals/admin/signature-collection/src/lib/messages.ts b/libs/portals/admin/signature-collection/src/lib/messages.ts index 2d997e5699ee..e9628478765f 100644 --- a/libs/portals/admin/signature-collection/src/lib/messages.ts +++ b/libs/portals/admin/signature-collection/src/lib/messages.ts @@ -195,17 +195,17 @@ export const m = defineMessages({ /* Hætta við söfnun modal */ cancelCollectionButton: { id: 'dmin-portal.signature-collection:cancelCollectionButton', - defaultMessage: 'Hætta við söfnun meðmæla', + defaultMessage: 'Eyða lista', description: '', }, cancelCollectionModalMessage: { id: 'dmin-portal.signature-collection:cancelCollectionModalMessage', - defaultMessage: 'Þú ert að fara að hætta við söfnun meðmæla. Ertu viss?', + defaultMessage: 'Þú ert að fara að eyða þessum lista. Ertu viss?', description: '', }, cancelCollectionModalConfirmButton: { id: 'dmin-portal.signature-collection:modalConfirmButton', - defaultMessage: 'Já, hætta við', + defaultMessage: 'Já, eyða lista', description: '', }, cancelCollectionModalCancelButton: { @@ -215,12 +215,12 @@ export const m = defineMessages({ }, cancelCollectionModalToastError: { id: 'dmin-portal.signature-collection:modalToastError', - defaultMessage: 'Ekki tókst að hætta við söfnun meðmæla', + defaultMessage: 'Ekki tókst að eyða lista', description: '', }, cancelCollectionModalToastSuccess: { id: 'dmin-portal.signature-collection:cancelCollectionModalToastSuccess', - defaultMessage: 'Tókst að hætta við söfnun meðmæla', + defaultMessage: 'Tókst að eyða lista', description: '', }, @@ -460,6 +460,11 @@ export const m = defineMessages({ defaultMessage: 'Samtals fjöldi', description: '', }, + totalListsPerConstituency: { + id: 'admin-portal.signature-collection:totalListsPerConstituency', + defaultMessage: 'Fjöldi lista: ', + description: '', + }, nationalIdsSuccess: { id: 'admin-portal.signature-collection:nationalIdsSuccess', defaultMessage: 'Kennitölur sem tókst að hlaða upp', diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx index b9abe304a76f..bc219cc5e686 100644 --- a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx @@ -23,7 +23,7 @@ const ParliamentaryRoot = () => { const { formatMessage } = useLocale() const navigate = useNavigate() - const { collection } = useLoaderData() as ListsLoaderReturn + const { collection, allLists } = useLoaderData() as ListsLoaderReturn return ( @@ -82,7 +82,10 @@ const ParliamentaryRoot = () => { {collection?.areas.map((area) => ( l.area.name === area.name).length + } heading={area.name} cta={{ label: formatMessage(m.viewConstituency), diff --git a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx index df7b95016cf0..8d3790d1f9fc 100644 --- a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx @@ -21,6 +21,9 @@ const SignedList = ({ useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() const [modalIsOpen, setModalIsOpen] = useState(false) + const [listIdToUnsign, setListIdToUnsign] = useState( + undefined, + ) // SignedList is typically singular, although it may consist of multiple entries, which in that case will all be invalid const { signedLists, loadingSignedLists, refetchSignedLists } = @@ -29,10 +32,7 @@ const SignedList = ({ const [unSign, { loading }] = useMutation(unSignList, { variables: { input: { - listId: - signedLists && signedLists?.length === 1 - ? signedLists[0].id - : undefined, + listId: listIdToUnsign, }, }, }) @@ -85,7 +85,10 @@ const SignedList = ({ variant: 'text', colorScheme: 'destructive', }, - onClick: () => setModalIsOpen(true), + onClick: () => { + setListIdToUnsign(list.id) + setModalIsOpen(true) + }, icon: undefined, } : undefined @@ -120,41 +123,44 @@ const SignedList = ({ : undefined } /> - setModalIsOpen(false)} - > - - - {formatMessage(m.unSignList)} - - - {formatMessage(m.unSignModalMessage)} - - - - - - ) })} + { + setListIdToUnsign(undefined) + setModalIsOpen(false) + }} + > + + + {formatMessage(m.unSignList)} + + + {formatMessage(m.unSignModalMessage)} + + + + + + )} From 60a4187fcf2acd7a1e8be7fd8d301af9eeaf191c Mon Sep 17 00:00:00 2001 From: juni-haukur <158475136+juni-haukur@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:43:11 +0000 Subject: [PATCH 156/173] feat(signature-collection): New endpoint to update signature page number (#16163) * New endpoint to update signature page number * Fix tests after client update --------- Co-authored-by: albinagu <47886428+albinagu@users.noreply.github.com> Co-authored-by: kksteini --- .../src/lib/dto/signatureUpdate.input.ts | 10 +++ .../lib/signatureCollectionAdmin.resolver.ts | 13 +++ .../lib/signatureCollectionAdmin.service.ts | 12 +++ .../src/clientConfig.json | 79 ++++++++++++++++++- .../lib/signature-collection-admin.service.ts | 19 +++++ .../lib/signature-collection.service.spec.ts | 25 ++++-- 6 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts diff --git a/libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts b/libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts new file mode 100644 index 000000000000..540738fa59c2 --- /dev/null +++ b/libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts @@ -0,0 +1,10 @@ +import { IsNumber } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql' +import { SignatureCollectionSignatureIdInput } from './signatureId.input' + +@InputType() +export class SignatureCollectionSignatureUpdateInput extends SignatureCollectionSignatureIdInput { + @Field() + @IsNumber() + pageNumber!: number +} diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts index 29986111d194..210711f3113c 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts @@ -34,6 +34,7 @@ import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input' import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input' import { ReasonKey } from '@island.is/clients/signature-collection' import { CanSignInfo } from './models/canSignInfo.model' +import { SignatureCollectionSignatureUpdateInput } from './dto/signatureUpdate.input' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(AdminPortalScope.signatureCollectionProcess) @@ -241,4 +242,16 @@ export class SignatureCollectionAdminResolver { ): Promise { return this.signatureCollectionService.compareLists(input, user) } + + @Mutation(() => SignatureCollectionSuccess) + @Audit() + async signatureCollectionAdminUpdatePaperSignaturePageNumber( + @CurrentUser() user: User, + @Args('input') input: SignatureCollectionSignatureUpdateInput, + ): Promise { + return this.signatureCollectionService.updateSignaturePageNumber( + user, + input, + ) + } } diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts index baa2b6bb7dad..10fa8f783fa0 100644 --- a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts +++ b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts @@ -21,6 +21,7 @@ import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input' import { SignatureCollectionSlug } from './models/slug.model' import { SignatureCollectionListStatus } from './models/status.model' import { SignatureCollectionIdInput } from './dto/collectionId.input' +import { SignatureCollectionSignatureUpdateInput } from './dto/signatureUpdate.input' @Injectable() export class SignatureCollectionAdminService { @@ -176,4 +177,15 @@ export class SignatureCollectionAdminService { user, ) } + + async updateSignaturePageNumber( + user: User, + input: SignatureCollectionSignatureUpdateInput, + ): Promise { + return await this.signatureCollectionClientService.updateSignaturePageNumber( + user, + input.signatureId, + input.pageNumber, + ) + } } diff --git a/libs/clients/signature-collection/src/clientConfig.json b/libs/clients/signature-collection/src/clientConfig.json index edf7e250b0cd..4928aa94d947 100644 --- a/libs/clients/signature-collection/src/clientConfig.json +++ b/libs/clients/signature-collection/src/clientConfig.json @@ -74,6 +74,44 @@ } } }, + "/Admin/Medmaeli/{ID}/UpdateBls": { + "patch": { + "tags": ["Admin"], + "summary": "Uppfærir blaðsíðunúmer skriflegs meðmælis", + "description": "Aðeins m0gulegt fyrir skrifleg meðmæli", + "parameters": [ + { + "name": "ID", + "in": "path", + "description": "ID meðmælis sem á að uppfæra", + "required": true, + "schema": { "type": "integer", "format": "int32" } + }, + { + "name": "blsNr", + "in": "query", + "description": "Nýtt blaðsíðutal", + "schema": { "type": "integer", "format": "int32" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/MedmaeliBaseDTO" } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { "schema": { "type": "string" } } + } + } + } + } + }, "/Admin/Medmaelalisti/{ID}": { "delete": { "tags": ["Admin"], @@ -1722,6 +1760,44 @@ } } }, + "/Medmaeli/{ID}/UpdateBls": { + "patch": { + "tags": ["Medmaeli"], + "summary": "Uppfærir blaðsíðunúmer skriflegs meðmælis", + "description": "Aðeins m0gulegt fyrir skrifleg meðmæli", + "parameters": [ + { + "name": "ID", + "in": "path", + "description": "ID meðmælis sem á að uppfæra", + "required": true, + "schema": { "type": "integer", "format": "int32" } + }, + { + "name": "blsNr", + "in": "query", + "description": "Nýtt blaðsíðutal", + "schema": { "type": "integer", "format": "int32" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/MedmaeliBaseDTO" } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { "schema": { "type": "string" } } + } + } + } + } + }, "/Tegund/Kosning": { "get": { "tags": ["Tegund"], @@ -2202,11 +2278,12 @@ "additionalProperties": false }, "SvaediDTO": { - "required": ["nafn", "svaediTegundLysing"], + "required": ["nafn", "nr", "svaediTegundLysing"], "type": "object", "properties": { "id": { "type": "integer", "format": "int32" }, "nafn": { "type": "string" }, + "nr": { "type": "string" }, "svaediTegund": { "type": "integer", "format": "int32" }, "svaediTegundLysing": { "type": "string" }, "fjoldi": { "type": "integer", "format": "int32" }, diff --git a/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts b/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts index d692dc89a01e..d6ba1183faf0 100644 --- a/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts +++ b/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts @@ -293,4 +293,23 @@ export class SignatureCollectionAdminClientService { return { success: false, reasons: [ReasonKey.DeniedByService] } } } + + async updateSignaturePageNumber( + auth: Auth, + signatureId: string, + pageNumber: number, + ): Promise { + try { + const res = await this.getApiWithAuth( + this.signatureApi, + auth, + ).medmaeliIDUpdateBlsPatch({ + iD: parseInt(signatureId), + blsNr: pageNumber, + }) + return { success: res.bladsidaNr === pageNumber } + } catch { + return { success: false } + } + } } diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts index 30ffd86f9c2d..22fb2aaa1b9f 100644 --- a/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts +++ b/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts @@ -26,7 +26,7 @@ const sofnun: MedmaelasofnunExtendedDTO[] = [ id: 123, sofnunStart: new Date('01.01.1900'), sofnunEnd: new Date('01.01.2199'), - svaedi: [{ id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' }], + svaedi: [{ id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing', nr: '1' }], frambodList: [{ id: 123, kennitala: '0101010119', nafn: 'Jónsframboð' }], kosning: { id: 123, @@ -40,7 +40,7 @@ const sofnun: MedmaelasofnunExtendedDTO[] = [ }, ] const sofnunUser: EinstaklingurKosningInfoDTO = { - svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' }, + svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing', nr: '1' }, kennitala: '0101302399', maFrambod: true, maFrambodInfo: { aldur: true, rikisfang: true, kennitala: '0101302399' }, @@ -143,7 +143,12 @@ describe('MyService', () => { kosningTegund: 'Forsetakosning', }, frambod: { id: 123, kennitala: '0101016789', nafn: 'Jónsframboð' }, - svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' }, + svaedi: { + id: 123, + nafn: 'Svæði', + svaediTegundLysing: 'Lýsing', + nr: '1', + }, dagsetningLokar: new Date('01.01.2199'), listaLokad: false, frambodNafn: 'Jónsframboð', @@ -158,7 +163,12 @@ describe('MyService', () => { kosningTegund: 'Forsetakosning', }, frambod: { id: 321, kennitala: '0202026789', nafn: 'Jónsframboð' }, - svaedi: { id: 321, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' }, + svaedi: { + id: 321, + nafn: 'Svæði', + svaediTegundLysing: 'Lýsing', + nr: '1', + }, dagsetningLokar: new Date('01.01.1900'), listaLokad: true, frambodNafn: 'Jónsframboð', @@ -243,7 +253,12 @@ describe('MyService', () => { sofnunEnd: new Date('01.01.2199'), }, frambod: { id: 123, kennitala: '0101016789', nafn: 'Jónsframboð' }, - svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' }, + svaedi: { + id: 123, + nafn: 'Svæði', + svaediTegundLysing: 'Lýsing', + nr: '1', + }, dagsetningLokar: new Date('01.01.2199'), listaLokad: false, frambodNafn: 'Jónsframboð', From e6601c00ae0b17763b319338c735ba3da101bdc2 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:39:17 +0000 Subject: [PATCH 157/173] fix(admin): parliamentary - edit page (#16179) * fix(admin): parliamentary - edit page * forgot filet * cleanup --- .../signature-collection/src/lib/messages.ts | 10 ++++++ .../src/screens-parliamentary/index.tsx | 3 -- .../completeReview/index.tsx | 32 +++++++++---------- .../signees/editPage/editPage.graphql | 7 ++++ .../{editPage.tsx => editPage/index.tsx} | 31 +++++++++++++++--- .../src/shared-components/signees/index.tsx | 1 + 6 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 libs/portals/admin/signature-collection/src/shared-components/signees/editPage/editPage.graphql rename libs/portals/admin/signature-collection/src/shared-components/signees/{editPage.tsx => editPage/index.tsx} (72%) diff --git a/libs/portals/admin/signature-collection/src/lib/messages.ts b/libs/portals/admin/signature-collection/src/lib/messages.ts index e9628478765f..f8381a48deb3 100644 --- a/libs/portals/admin/signature-collection/src/lib/messages.ts +++ b/libs/portals/admin/signature-collection/src/lib/messages.ts @@ -568,6 +568,16 @@ export const m = defineMessages({ defaultMessage: 'Breyta blaðsíðunúmeri', description: '', }, + editPaperNumberSuccess: { + id: 'admin-portal.signature-collection:editPaperNumberSuccess', + defaultMessage: 'Tókst að breyta blaðsíðunúmeri', + description: '', + }, + editPaperNumberError: { + id: 'admin-portal.signature-collection:editPaperNumberSuccess', + defaultMessage: 'Ekki tókst að breyta blaðsíðunúmeri', + description: '', + }, saveEditPaperNumber: { id: 'admin-portal.signature-collection:saveEditPaperNumber', defaultMessage: 'Uppfæra blaðsíðunúmer', diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx index bc219cc5e686..c422b85c122b 100644 --- a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx @@ -75,9 +75,6 @@ const ParliamentaryRoot = () => { - - {formatMessage(m.totalListResults)}: {collection?.areas.length} - {collection?.areas.map((area) => ( - - - - + + + { const { formatMessage } = useLocale() const [newPage, setNewPage] = useState(page) const [modalIsOpen, setModalIsOpen] = useState(false) + const { revalidate } = useRevalidator() + + const [updatePage, { loading }] = + useSignatureCollectionAdminUpdatePaperSignaturePageNumberMutation({ + variables: { + input: { + pageNumber: newPage, + signatureId: signatureId, + }, + }, + onCompleted: () => { + toast.success(formatMessage(m.editPaperNumberSuccess)) + revalidate() + setModalIsOpen(false) + }, + onError: () => { + toast.error(formatMessage(m.editPaperNumberError)) + }, + }) return ( @@ -74,9 +98,8 @@ const EditPage = ({ diff --git a/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx b/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx index a29b504a2bcf..b69581ddd8e4 100644 --- a/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx +++ b/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx @@ -126,6 +126,7 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { page={s.pageNumber ?? 0} name={s.signee.name} nationalId={formatNationalId(s.signee.nationalId)} + signatureId={s.id} /> )} From 55d69bb445330486a043821357a2464b31e28c91 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:47:42 +0000 Subject: [PATCH 158/173] fix(signature-collection): sign list done screen prettier (#16178) * fix(signature-collection): sign list done screen prettier * chore: nx format:write update dirty files --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: andes-it --- .../assets/ManOnBenchIllustration.tsx | 450 ++++++++++++++++++ .../src/forms/Done.ts | 37 +- .../src/lib/messages.ts | 2 +- 3 files changed, 477 insertions(+), 12 deletions(-) create mode 100644 libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx b/libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx new file mode 100644 index 000000000000..7139eb016608 --- /dev/null +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx @@ -0,0 +1,450 @@ +const Man = () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + +) + +export const ManOnBenchIllustration = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) 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 e65e617c9965..7df9cd082957 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 @@ -4,11 +4,14 @@ import { buildSection, buildMessageWithLinkButtonField, buildDescriptionField, + buildImageField, + buildAlertMessageField, } from '@island.is/application/core' import { Application, Form, FormModes } from '@island.is/application/types' import { m } from '../lib/messages' import { infer as zinfer } from 'zod' import { dataSchema } from '../lib/dataSchema' +import { ManOnBenchIllustration } from '../../assets/ManOnBenchIllustration' type Answers = zinfer export const Done: Form = buildForm({ @@ -35,25 +38,37 @@ export const Done: Form = buildForm({ buildMultiField({ id: 'doneScreen', title: m.listSigned, - description: (application: Application) => ({ - ...m.listSignedDescription, - values: { - name: (application.answers as Answers).list.name, - }, - }), children: [ - buildMessageWithLinkButtonField({ - id: 'done.goToServicePortal', + buildAlertMessageField({ + id: 'doneAlertMessage', title: '', - url: '/minarsidur/min-gogn/listar/althingis-medmaelasofnun', - buttonTitle: m.linkFieldButtonTitle, - message: m.linkFieldMessage, + message: (application: Application) => ({ + ...m.listSignedDescription, + values: { + name: (application.answers as Answers).list.name, + }, + }), + alertType: 'success', + }), + buildImageField({ + id: 'doneImage', + title: '', + image: ManOnBenchIllustration, + imageWidth: '50%', + imagePosition: 'center', }), buildDescriptionField({ id: 'space', title: '', space: 'containerGutter', }), + buildMessageWithLinkButtonField({ + id: 'done.goToServicePortal', + title: '', + url: '/minarsidur/min-gogn/listar/althingis-medmaelasofnun', + buttonTitle: m.linkFieldButtonTitle, + message: m.linkFieldMessage, + }), buildDescriptionField({ id: 'space1', title: '', diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts index ff0e5a3a8eef..1c24b6731cef 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts @@ -206,7 +206,7 @@ export const m = defineMessages({ }, linkFieldMessage: { id: 'pls.application:linkFieldMessage', - defaultMessage: 'Á Mínum síðum geturðu séð hvaða framboði þú mældir með', + defaultMessage: 'Á Mínum síðum geturðu séð hvaða framboði þú mæltir með', description: '', }, From 3a56c8c7bd4da24958a1e00b044dda74b699277c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20Gu=C3=B0mundsson?= <34029342+GunnlaugurG@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:40:16 +0000 Subject: [PATCH 159/173] feat(delegation-api): Add GeneralMandate delegations to accessControl (#16170) * delegation-to for access control * performance fixes * fix zendesk constants * small refactor * small refactor --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/delegations/constants/zendesk.ts | 4 +- .../delegations-outgoing.service.ts | 96 ++++++++++++------- .../src/components/access/AccessCard.tsx | 14 +-- 3 files changed, 74 insertions(+), 40 deletions(-) diff --git a/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts b/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts index b6e4ba2f6cd6..75b1df95fc5c 100644 --- a/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts +++ b/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts @@ -1,5 +1,5 @@ export const DELEGATION_TAG = 'umsokn_um_umboð_a_mínum_síðum' export const ZENDESK_CUSTOM_FIELDS = { - DelegationFromReferenceId: 21401464004498, - DelegationToReferenceId: 21401435545234, + DelegationToReferenceId: 21401464004498, + DelegationFromReferenceId: 21401435545234, } diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts index ab615439e5fb..807298a7fa2a 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts @@ -8,6 +8,7 @@ import { import { InjectModel } from '@nestjs/sequelize' import { and, Op, WhereOptions } from 'sequelize' import { isUuid, uuid } from 'uuidv4' +import startOfDay from 'date-fns/startOfDay' import { User } from '@island.is/auth-nest-tools' import { NoContentException } from '@island.is/nest/problem' @@ -39,6 +40,8 @@ import { import { Features } from '@island.is/feature-flags' import { FeatureFlagService } from '@island.is/nest/feature-flags' import { LOGGER_PROVIDER } from '@island.is/logging' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' +import { AuthDelegationType } from '@island.is/shared/types' /** * Service class for outgoing delegations. @@ -68,42 +71,71 @@ export class DelegationsOutgoingService { if (otherUser) { return this.findByOtherUser(user, otherUser, domainName) } - const delegations = await this.delegationModel.findAll({ - where: and( - { + + const [delegations, delegationTypesDelegations] = await Promise.all([ + this.delegationModel.findAll({ + where: and( + { + fromNationalId: user.nationalId, + }, + domainName ? { domainName } : {}, + getDelegationNoActorWhereClause(user), + ...(await this.delegationResourceService.apiScopeFilter({ + user, + prefix: 'delegationScopes->apiScope', + direction: DelegationDirection.OUTGOING, + })), + ), + include: [ + { + model: DelegationScope, + include: [ + { + attributes: ['displayName'], + model: ApiScope, + required: true, + include: [ + ...this.delegationResourceService.apiScopeInclude( + user, + DelegationDirection.OUTGOING, + ), + ], + }, + ], + required: validity !== DelegationValidity.ALL, + where: getScopeValidityWhereClause(validity), + }, + ], + }), + this.delegationModel.findAll({ + where: { fromNationalId: user.nationalId, }, - domainName ? { domainName } : {}, - getDelegationNoActorWhereClause(user), - ...(await this.delegationResourceService.apiScopeFilter({ - user, - prefix: 'delegationScopes->apiScope', - direction: DelegationDirection.OUTGOING, - })), - ), - include: [ - { - model: DelegationScope, - include: [ - { - attributes: ['displayName'], - model: ApiScope, - required: true, - include: [ - ...this.delegationResourceService.apiScopeInclude( - user, - DelegationDirection.OUTGOING, - ), - ], + include: [ + { + model: DelegationDelegationType, + where: { + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: { + [Op.or]: { + [Op.gte]: startOfDay(new Date()), + [Op.is]: null, + }, + }, }, - ], - required: validity !== DelegationValidity.ALL, - where: getScopeValidityWhereClause(validity), - }, - ], - }) + required: true, + }, + ], + }), + ]) + + const delegationTypesDTO = delegationTypesDelegations.map((d) => + d.toDTO(AuthDelegationType.GeneralMandate), + ) + + const delegationsDTO = delegations.map((d) => d.toDTO()) - return delegations.map((d) => d.toDTO()) + return [...delegationsDTO, ...delegationTypesDTO] } async findByOtherUser( diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx index 1537f63de36e..b6f48d880d89 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx @@ -5,13 +5,13 @@ import * as kennitala from 'kennitala' import { Box, - Text, - Stack, - Tag, - Inline, + Button, Icon, IconMapIcon as IconType, - Button, + Inline, + Stack, + Tag, + Text, Tooltip, } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' @@ -182,7 +182,9 @@ export const AccessCard = ({ - {(isAdminView || !isOutgoing) && ( + {(isAdminView || + !isOutgoing || + delegation.type === AuthDelegationType.GeneralMandate) && ( <> {renderDelegationTypeLabel(delegation.type)} {delegation.domain?.name && ( From 149434332f2a15eebd706edbf9f7639e21e98917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Fri, 27 Sep 2024 10:47:49 +0000 Subject: [PATCH 160/173] chore(regulations-admin): Update presigned url for file uploader (#16106) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../admin/regulations-admin/src/utils/fileUploader.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/libs/portals/admin/regulations-admin/src/utils/fileUploader.ts b/libs/portals/admin/regulations-admin/src/utils/fileUploader.ts index c4559ddaf114..acd0e4f6b217 100644 --- a/libs/portals/admin/regulations-admin/src/utils/fileUploader.ts +++ b/libs/portals/admin/regulations-admin/src/utils/fileUploader.ts @@ -1,14 +1,10 @@ import { EditorFileUploader } from '@island.is/regulations-tools/Editor' import { RegulationDraftId } from '@island.is/regulations/admin' import { fileUrl, useS3Upload } from './dataHooks' -import { isRunningOnEnvironment } from '@island.is/shared/utils' export function useFileUploader(draftId: RegulationDraftId) { const { createPresignedPost, createFormData } = useS3Upload() - const isDevelopment = - isRunningOnEnvironment('dev') || isRunningOnEnvironment('local') - const fileUploader = (): EditorFileUploader => async (blobInfo, success, failure, progress) => { try { @@ -47,11 +43,7 @@ export function useFileUploader(draftId: RegulationDraftId) { // Create FormData and send the request const formData = createFormData(presignedPost, blob as File) - request.open( - 'POST', - `${isDevelopment ? presignedPost?.url : fileUrl + '/'}`, - true, - ) + request.open('POST', presignedPost?.url, true) request.send(formData) } catch (error) { console.error('Error during upload:', error) From 5efe4fddc4e7fb3de68e8ee7cefbb5535e4087dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Fri, 27 Sep 2024 13:12:14 +0200 Subject: [PATCH 161/173] chore(j-s): Indictment Case Arrignment Date (#16156) * Locks subpoena fields when arraignment date has been set * Move arraignment date message handling to the server side * Updates tests and fixes date comparison --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/app/modules/case/case.service.ts | 46 +++++++++++++++--- .../case/test/caseController/update.spec.ts | 48 ++++++++++++++++++- .../notification/notification.service.ts | 9 ---- .../app/modules/subpoena/subpoena.service.ts | 1 + .../Court/Indictments/Subpoena/Subpoena.tsx | 24 +--------- 5 files changed, 87 insertions(+), 41 deletions(-) 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 60896a1422b8..65ad33c4d2ee 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 @@ -1146,15 +1146,29 @@ export class CaseService { private addMessagesForNewCourtDateToQueue( theCase: Case, user: TUser, + arraignmentDateChanged: boolean, ): Promise { - return this.messageService.sendMessagesToQueue([ + const messages: Message[] = [ { type: MessageType.NOTIFICATION, user, caseId: theCase.id, body: { type: NotificationType.COURT_DATE }, }, - ]) + ] + + if (arraignmentDateChanged) { + theCase.defendants?.forEach((defendant) => { + messages.push({ + type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, + user, + caseId: theCase.id, + elementId: defendant.id, + }) + }) + } + + return this.messageService.sendMessagesToQueue(messages) } private async addMessagesForUpdatedCaseToQueue( @@ -1313,11 +1327,29 @@ export class CaseService { } // This only applies to indictments - const courtDate = DateLog.courtDate(theCase.dateLogs) - const updatedCourtDate = DateLog.courtDate(updatedCase.dateLogs) - if (updatedCourtDate && updatedCourtDate.date !== courtDate?.date) { - // New court date - await this.addMessagesForNewCourtDateToQueue(updatedCase, user) + if (isIndictment) { + const arraignmentDate = DateLog.arraignmentDate(theCase.dateLogs) + const updatedArraignmentDate = DateLog.arraignmentDate( + updatedCase.dateLogs, + ) + const arraignmentDateChanged = + updatedArraignmentDate && + updatedArraignmentDate.date.getTime() !== + arraignmentDate?.date.getTime() + const courtDate = DateLog.courtDate(theCase.dateLogs) + const updatedCourtDate = DateLog.courtDate(updatedCase.dateLogs) + const courtDateChanged = + updatedCourtDate && + updatedCourtDate.date.getTime() !== courtDate?.date.getTime() + + if (arraignmentDateChanged || courtDateChanged) { + // New arraignment date or new court date + await this.addMessagesForNewCourtDateToQueue( + updatedCase, + user, + Boolean(arraignmentDateChanged), + ) + } } } diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts index 0ffde6ad16ca..932319cac89e 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts @@ -872,11 +872,56 @@ describe('CaseController - Update', () => { }) }) - describe('court date updated', () => { + describe('indictment arraignment date updated', () => { + const arraignmentDate = { date: new Date(), location: uuid() } + const caseToUpdate = { arraignmentDate } + const updatedCase = { + ...theCase, + type: CaseType.INDICTMENT, + dateLogs: [{ dateType: DateType.ARRAIGNMENT_DATE, ...arraignmentDate }], + } + + beforeEach(async () => { + const mockFindOne = mockCaseModel.findOne as jest.Mock + mockFindOne.mockResolvedValueOnce(updatedCase) + + await givenWhenThen(caseId, user, theCase, caseToUpdate) + }) + + it('should update case', () => { + expect(mockDateLogModel.create).toHaveBeenCalledWith( + { dateType: DateType.ARRAIGNMENT_DATE, caseId, ...arraignmentDate }, + { transaction }, + ) + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.NOTIFICATION, + user, + caseId, + body: { type: NotificationType.COURT_DATE }, + }, + { + type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, + user, + caseId: theCase.id, + elementId: defendantId1, + }, + { + type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, + user, + caseId: theCase.id, + elementId: defendantId2, + }, + ]) + }) + }) + + describe('indictment court date updated', () => { const courtDate = { date: new Date(), location: uuid() } const caseToUpdate = { courtDate } const updatedCase = { ...theCase, + type: CaseType.INDICTMENT, dateLogs: [{ dateType: DateType.COURT_DATE, ...courtDate }], } @@ -892,7 +937,6 @@ describe('CaseController - Update', () => { { dateType: DateType.COURT_DATE, caseId, ...courtDate }, { transaction }, ) - expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ { type: MessageType.NOTIFICATION, 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 e4ffc288777d..8103b1106f14 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,15 +69,6 @@ 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/subpoena/subpoena.service.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts index 62d0355fd57c..c28cf4ec4875 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts @@ -126,6 +126,7 @@ export class SubpoenaService { return { delivered: false } } + // TODO: Improve error handling by checking how many rows were affected and posting error event await this.subpoenaModel.update( { subpoenaId: createdSubpoena.subpoenaId }, { where: { id: subpoena.id } }, diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx index 4d13fb753f1c..11c767d7008f 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx @@ -19,14 +19,9 @@ import { SectionHeading, useCourtArrangements, } from '@island.is/judicial-system-web/src/components' -import { NotificationType } from '@island.is/judicial-system-web/src/graphql/schema' import { SubpoenaType } from '@island.is/judicial-system-web/src/routes/Court/components' import type { stepValidationsType } from '@island.is/judicial-system-web/src/utils/formHelper' -import { - useCase, - useDefendants, -} from '@island.is/judicial-system-web/src/utils/hooks' -import { hasSentNotification } from '@island.is/judicial-system-web/src/utils/stepHelper' +import { useDefendants } from '@island.is/judicial-system-web/src/utils/hooks' import { isSubpoenaStepValid } from '@island.is/judicial-system-web/src/utils/validate' import { subpoena as strings } from './Subpoena.strings' @@ -39,12 +34,10 @@ const Subpoena: FC = () => { const { formatMessage } = useIntl() const { courtDate, - courtDateHasChanged, handleCourtDateChange, handleCourtRoomChange, sendCourtDateToServer, } = useCourtArrangements(workingCase, setWorkingCase, 'arraignmentDate') - const { sendNotification } = useCase() const isArraignmentScheduled = Boolean(workingCase.arraignmentDate) @@ -69,18 +62,6 @@ const Subpoena: FC = () => { }) } - if ( - !hasSentNotification( - NotificationType.COURT_DATE, - workingCase.notifications, - ).hasSent || - courtDateHasChanged - ) { - promises.push( - sendNotification(workingCase.id, NotificationType.COURT_DATE), - ) - } - const allDataSentToServer = await Promise.all(promises) if (!allDataSentToServer.every((result) => result)) { return @@ -92,11 +73,8 @@ const Subpoena: FC = () => { isArraignmentScheduled, sendCourtDateToServer, workingCase.defendants, - workingCase.notifications, workingCase.id, - courtDateHasChanged, updateDefendant, - sendNotification, ], ) From a8b2817ba215f09ca9c2f472b2be8073d3052db8 Mon Sep 17 00:00:00 2001 From: valurefugl <65780958+valurefugl@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:48:52 +0100 Subject: [PATCH 162/173] feat(ids-api): Add feature flag for general mandate delegation type. (#16182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add feature flag for general mandate delegation type. * Flip featureFlagService.getValue to be default false --------- Co-authored-by: Sævar Már Atlason Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../delegations-incoming.service.ts | 22 +++++++++++++------ libs/feature-flags/src/lib/features.ts | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts index 56355428a63e..ca782439de6c 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts @@ -189,13 +189,21 @@ export class DelegationsIncomingService { } if (types?.includes(AuthDelegationType.GeneralMandate)) { - delegationPromises.push( - this.delegationsIncomingCustomService.findAllAvailableGeneralMandate( + const isGeneralMandateDelegationEnabled = + await this.featureFlagService.getValue( + Features.isGeneralMandateDelegationEnabled, + false, user, - clientAllowedApiScopes, - client.requireApiScopes, - ), - ) + ) + if (isGeneralMandateDelegationEnabled) { + delegationPromises.push( + this.delegationsIncomingCustomService.findAllAvailableGeneralMandate( + user, + clientAllowedApiScopes, + client.requireApiScopes, + ), + ) + } } if ( @@ -220,7 +228,7 @@ export class DelegationsIncomingService { const isLegalRepresentativeDelegationEnabled = await this.featureFlagService.getValue( Features.isLegalRepresentativeDelegationEnabled, - true, + false, user, ) if (isLegalRepresentativeDelegationEnabled) { diff --git a/libs/feature-flags/src/lib/features.ts b/libs/feature-flags/src/lib/features.ts index eb5bde4c7e7d..fd67ac6db300 100644 --- a/libs/feature-flags/src/lib/features.ts +++ b/libs/feature-flags/src/lib/features.ts @@ -107,6 +107,9 @@ export enum Features { // Legal represantative delegation type isLegalRepresentativeDelegationEnabled = 'isLegalRepresentativeDelegationEnabled', + + // General mandate delegation type + isGeneralMandateDelegationEnabled = 'isGeneralMandateDelegationEnabled', } export enum ServerSideFeature { From 6d4e0306d8903c3e07d98fe398168c787b3be261 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:52:18 +0000 Subject: [PATCH 163/173] feat(web): Add default header for HSA organization (#16131) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Wrapper/OrganizationWrapper.css.ts | 23 +++++++++++++++++++ .../Wrapper/OrganizationWrapper.tsx | 11 ++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts index 5ccd9797f6b2..13d010320762 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts @@ -105,6 +105,29 @@ export const rikissaksoknariHeaderGridContainerWidth = style([ export const rikissaksoknariHeaderGridContainerSubpage = rikissaksoknariHeaderGridContainerBase +export const hsaHeaderGridContainerBase = style({ + display: 'grid', + maxWidth: '1342px', + margin: '0 auto', + ...themeUtils.responsiveStyle({ + lg: { + gridTemplateRows: '315px', + gridTemplateColumns: '65fr 35fr', + }, + }), +}) + +export const hsaHeaderGridContainerWidthSubpage = hsaHeaderGridContainerBase + +export const hsaHeaderGridContainerWidth = style([ + hsaHeaderGridContainerBase, + themeUtils.responsiveStyle({ + lg: { + background: `url('https://images.ctfassets.net/8k0h54kbe6bj/uc45ywvPOYsIUEQTNfE6s/72fd0f2229407e18c6e2908fb13f51c3/Header_HSA.png') no-repeat right bottom,linear-gradient(90deg, #CCDFFF 0%, #F6F6F6 84.85%)`, + }, + }), +]) + export const rikislogmadurHeaderGridContainerWidthBase = style({ display: 'grid', maxWidth: '1342px', diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index af00b7732f18..56ce9ff733b2 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -478,7 +478,16 @@ export const OrganizationHeader: React.FC< /> ) case 'hsa': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Fri, 27 Sep 2024 14:02:35 +0000 Subject: [PATCH 164/173] fix(application-aod): Validation needed for dateofbirth (#16075) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../announcement-of-death/src/lib/dataSchema.ts | 15 +++++++++++++++ .../announcement-of-death/src/lib/messages.ts | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/libs/application/templates/announcement-of-death/src/lib/dataSchema.ts b/libs/application/templates/announcement-of-death/src/lib/dataSchema.ts index e1feabcf8435..14f4d0db21e7 100644 --- a/libs/application/templates/announcement-of-death/src/lib/dataSchema.ts +++ b/libs/application/templates/announcement-of-death/src/lib/dataSchema.ts @@ -118,6 +118,21 @@ export const dataSchema = z.object({ dateOfBirth: z.string().min(1).optional(), dummy: z.boolean().optional(), }) + .refine( + ({ name, relation, nationalId, foreignCitizenship, dateOfBirth }) => { + const hasNameAndRelation = name && relation + + if (foreignCitizenship && foreignCitizenship.length !== 0) { + return Boolean(dateOfBirth) && hasNameAndRelation + } else { + return Boolean(nationalId) && hasNameAndRelation + } + }, + { + message: m.errorNoDateOfBirthProvided.defaultMessage, + path: ['dateOfBirth'], + }, + ) .array() .optional(), encountered: z.boolean().optional(), diff --git a/libs/application/templates/announcement-of-death/src/lib/messages.ts b/libs/application/templates/announcement-of-death/src/lib/messages.ts index db3556c25385..ebbd3a17613f 100644 --- a/libs/application/templates/announcement-of-death/src/lib/messages.ts +++ b/libs/application/templates/announcement-of-death/src/lib/messages.ts @@ -558,6 +558,11 @@ Ef ekkert á við sem hér að ofan er talið rennur arfur í ríkissjóð. Nán defaultMessage: 'Númer má ekki vera tómt', description: 'Invalid general asset number error message', }, + errorNoDateOfBirthProvided: { + id: 'aod.application:error.errorNoDateOfBirthProvided', + defaultMessage: 'Fæðingardagur þarf að vera fylltur út', + description: 'Date of birth is required', + }, /* Announcement */ announcementTitle: { From 9095b025afd2925ad29b4023354293b6d51e6d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:35:02 +0000 Subject: [PATCH 165/173] fix(search-indexer): Limit what fields are indexed for "Latest Generic List Items" model (#16187) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../lib/models/latestGenericListItems.model.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/libs/cms/src/lib/models/latestGenericListItems.model.ts b/libs/cms/src/lib/models/latestGenericListItems.model.ts index 351712ccbbc6..3fe7a7a43103 100644 --- a/libs/cms/src/lib/models/latestGenericListItems.model.ts +++ b/libs/cms/src/lib/models/latestGenericListItems.model.ts @@ -44,13 +44,18 @@ const mapSeeMorePage = (seeMorePage: IOrganizationSubpage | undefined) => { return mapOrganizationSubpage({ ...seeMorePage, fields: { - ...seeMorePage.fields, + title: seeMorePage.fields?.title, + slug: seeMorePage.fields?.slug, organizationPage: { - ...seeMorePage.fields.organizationPage, + ...seeMorePage.fields?.organizationPage, fields: { - ...seeMorePage.fields.organizationPage?.fields, - slices: [], - bottomSlices: [], + title: seeMorePage.fields?.organizationPage?.fields?.title, + slug: seeMorePage.fields?.organizationPage?.fields?.slug, + featuredImage: + seeMorePage.fields?.organizationPage?.fields?.featuredImage, + organization: + seeMorePage.fields?.organizationPage?.fields?.organization, + theme: seeMorePage.fields?.organizationPage?.fields?.theme, }, }, }, From dc0e0feca551c35749546f2f48807472ad5eec10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Fri, 27 Sep 2024 15:09:15 +0000 Subject: [PATCH 166/173] fix(native-app): don't show progress bar for applications with type in progress (#16158) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../applications/components/applications-preview.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/native/app/src/screens/applications/components/applications-preview.tsx b/apps/native/app/src/screens/applications/components/applications-preview.tsx index 38d3948e7ca3..d9bcbcfd5c55 100644 --- a/apps/native/app/src/screens/applications/components/applications-preview.tsx +++ b/apps/native/app/src/screens/applications/components/applications-preview.tsx @@ -68,11 +68,15 @@ export const ApplicationsPreview = ({ /> } progress={ - type !== 'incomplete' - ? undefined - : application.actionCard?.draftFinishedSteps ?? 0 + type === 'incomplete' + ? application.actionCard?.draftFinishedSteps ?? 0 + : undefined + } + progressTotalSteps={ + type === 'incomplete' + ? application.actionCard?.draftTotalSteps ?? 0 + : undefined } - progressTotalSteps={application.actionCard?.draftTotalSteps ?? 0} progressMessage={intl.formatMessage( { id: 'applicationStatusCard.draftProgress', From 3df11faef0fce9c81ecd7e6467381b6a89fa8d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:48:21 +0000 Subject: [PATCH 167/173] fix(search-indexer): Reduce how much we write and read from elasticsearch (#16189) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/cms/src/lib/search/contentful.service.ts | 2 +- libs/content-search-toolkit/src/services/elastic.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/cms/src/lib/search/contentful.service.ts b/libs/cms/src/lib/search/contentful.service.ts index 8b14f7ac5f4c..52c92df3645a 100644 --- a/libs/cms/src/lib/search/contentful.service.ts +++ b/libs/cms/src/lib/search/contentful.service.ts @@ -475,7 +475,7 @@ export class ContentfulService { let idsChunk = idsCopy.splice(-MAX_REQUEST_COUNT, MAX_REQUEST_COUNT) while (idsChunk.length > 0) { - const size = 1000 + const size = 100 let page = 1 const items: string[] = [] diff --git a/libs/content-search-toolkit/src/services/elastic.service.ts b/libs/content-search-toolkit/src/services/elastic.service.ts index 3799eabdd04c..93f0fdf9436a 100644 --- a/libs/content-search-toolkit/src/services/elastic.service.ts +++ b/libs/content-search-toolkit/src/services/elastic.service.ts @@ -112,10 +112,10 @@ export class ElasticService { refresh = false, ) { try { - // elasticsearch does not like big requests (above 5mb) so we limit the size to X entries just in case const client = await this.getClient() - let requestChunk = getValidBulkRequestChunk(requests) + // elasticsearch does not like big requests (above 5mb) so we limit the size to X entries just in case + let requestChunk = getValidBulkRequestChunk(requests, 10) while (requestChunk.length) { // wait for request b4 continuing From 8fb3ddf24055e209a65ccc94ce22789a398ce8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Albert?= Date: Fri, 27 Sep 2024 16:13:42 +0000 Subject: [PATCH 168/173] chore(driving-license): minor text changes (#16186) * wip * wip * revert * revert --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../forms/prerequisites/sectionDigitalLicenseInfo.ts | 7 +++++++ .../templates/driving-license/src/lib/messages.ts | 12 +++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/application/templates/driving-license/src/forms/prerequisites/sectionDigitalLicenseInfo.ts b/libs/application/templates/driving-license/src/forms/prerequisites/sectionDigitalLicenseInfo.ts index 279e17edd222..af680c75640c 100644 --- a/libs/application/templates/driving-license/src/forms/prerequisites/sectionDigitalLicenseInfo.ts +++ b/libs/application/templates/driving-license/src/forms/prerequisites/sectionDigitalLicenseInfo.ts @@ -1,5 +1,6 @@ import { buildAlertMessageField, + buildDescriptionField, buildMultiField, buildSubSection, } from '@island.is/application/core' @@ -24,6 +25,12 @@ export const sectionDigitalLicenseInfo = buildSubSection({ : m.digitalLicenseInfoAlertMessageBFull, alertType: 'info', }), + buildDescriptionField({ + id: 'extraInfo', + title: '', + marginTop: 2, + description: m.digitalLicenseInfoAlertMessageExtraInfo, + }), ], }), ], diff --git a/libs/application/templates/driving-license/src/lib/messages.ts b/libs/application/templates/driving-license/src/lib/messages.ts index 41f59615efb6..fdda81aaa35d 100644 --- a/libs/application/templates/driving-license/src/lib/messages.ts +++ b/libs/application/templates/driving-license/src/lib/messages.ts @@ -122,18 +122,18 @@ export const m = defineMessages({ }, healthDeclarationSectionTitle: { id: 'dl.application:healthDeclarationSection.title', - defaultMessage: 'Heilbrigðisyfirlýsing', + defaultMessage: 'Læknisvottorð', description: 'Health declaration', }, healthDeclarationMultiFieldTitle: { id: 'dl.application:healthDeclarationMultiField.title', - defaultMessage: 'Heilbrigðisyfirlýsing', + defaultMessage: 'Læknisvottorð', description: 'Health declaration', }, healthDeclarationMultiField65Description: { id: 'dl.application:healthDeclarationMultiField65Description#markdown', defaultMessage: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nunc nec ultricies ultricies, nunc nisl ultricies nunc, nec ultricies nunc nisl nec nunc. Nullam auctor, nunc nec ultricies ultricies, nunc nisl ultricies nunc, nec ultricies nunc nisl nec nunc.', + 'Þú þarft að skila inn læknisvottorði vegna ökuleyfis til að endurnýja ökuskírteini þitt. Læknisvottorðið þarf að vera frá **heimilislækni** og vegna ökuleyfis. Þegar búið er að ljúka umsókn þarf að skila inn læknisvottorði á valið sýslumannsembætti til að hægt sé að panta skírteinið. **Athugið að skírteinið verður ekki pantað fyrr en búið er að skila inn vottorði.**', description: 'Health declaration', }, healthDeclarationMultiFieldSubTitle: { @@ -486,6 +486,12 @@ export const m = defineMessages({ 'Þú ert að sækja um fullnaðarökuskírteini. Ökuskírteini þitt verður núna einungis gefið út sem stafrænt ökuskírteini og verður aðgengilegt fyrir þig þegar þú hefur lokið þessari pöntun um fullnaðarökuskírteini. Fullnaðarökuskírteini þitt verður framleitt í plasti í byrjun febrúar 2025 og sent til þín með Póstinum, á skráð lögheimili þitt um leið og plastökuskírteinið er tilbúið.', description: 'Digital driving license', }, + digitalLicenseInfoAlertMessageExtraInfo: { + id: 'dl.application:digitalLicenseInfoAlertMessageExtraInfo#markdown', + defaultMessage: + 'Upplýsingar um stafrænt ökuskírteini, hvernig þú sækir það og hleður því í símannn þinn eru aðgengilegar hér [https://island.is/okuskirteini](https://island.is/okuskirteini)', + description: 'Digital driving license', + }, congratulationsTempHelpText: { id: 'dl.application:congratulationsTempHelpText', defaultMessage: From eac1952e331155ceb288eb637b51f584a8dc506c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Albert?= Date: Fri, 27 Sep 2024 16:39:45 +0000 Subject: [PATCH 169/173] chore(driving-license): quality photo check for 65+ (#16191) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/fields/QualityPhoto/QualityPhoto.tsx | 1 - .../src/forms/draft/subSectionQualityPhoto.ts | 2 +- .../driving-license/src/lib/utils/formUtils.ts | 13 ++++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/libs/application/templates/driving-license/src/fields/QualityPhoto/QualityPhoto.tsx b/libs/application/templates/driving-license/src/fields/QualityPhoto/QualityPhoto.tsx index 4190d4c2e3e8..4063f093a531 100644 --- a/libs/application/templates/driving-license/src/fields/QualityPhoto/QualityPhoto.tsx +++ b/libs/application/templates/driving-license/src/fields/QualityPhoto/QualityPhoto.tsx @@ -3,7 +3,6 @@ import React, { FC } from 'react' import { Box, Text, - ContentBlock, AlertMessage, SkeletonLoader, } from '@island.is/island-ui/core' diff --git a/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts b/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts index 0b598d74a8e9..351b04450050 100644 --- a/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts +++ b/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts @@ -20,7 +20,7 @@ export const subSectionQualityPhoto = buildSubSection({ id: 'photoStep', title: m.applicationQualityPhotoTitle, condition: isVisible( - isApplicationForCondition(B_FULL || B_FULL_RENEWAL_65), + isApplicationForCondition([B_FULL, B_FULL_RENEWAL_65]), hasNoDrivingLicenseInOtherCountry, ), children: [ diff --git a/libs/application/templates/driving-license/src/lib/utils/formUtils.ts b/libs/application/templates/driving-license/src/lib/utils/formUtils.ts index 792e143e011c..69897b5d9bae 100644 --- a/libs/application/templates/driving-license/src/lib/utils/formUtils.ts +++ b/libs/application/templates/driving-license/src/lib/utils/formUtils.ts @@ -36,14 +36,13 @@ export const isVisible = } export const isApplicationForCondition = - (result: DrivingLicenseApplicationFor) => (answers: FormValue) => { - const applicationFor = - getValueViaPath( - answers, - 'applicationFor', - ) ?? B_FULL + (result: DrivingLicenseApplicationFor | DrivingLicenseApplicationFor[]) => + (answers: FormValue) => { + const strings = Array.isArray(result) ? result : [result] - return applicationFor === result + return strings.some( + (x) => x === getValueViaPath(answers, 'applicationFor') ?? B_FULL, + ) } export const hasNoDrivingLicenseInOtherCountry = (answers: FormValue) => From 775256aa475db0b2e6fa1c8c0cf8ebd4791e4077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Fri, 27 Sep 2024 18:49:17 +0200 Subject: [PATCH 170/173] fix(j-s): National Id Lookup Check (#16190) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../routes/Prosecutor/Indictments/Processing/Processing.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx index 63d9972e6ca3..c76de5e43862 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Processing/Processing.tsx @@ -112,9 +112,7 @@ const Processing: FC = () => { civilClaimantNationalIdUpdate?.nationalId, ) - const stepIsValid = - isProcessingStepValidIndictments(workingCase) && - nationalIdNotFound === false + const stepIsValid = isProcessingStepValidIndictments(workingCase) const handleUpdateDefendant = useCallback( (updatedDefendant: UpdateDefendantInput) => { From a73abfa37faeee65dc563b26b251b596497accde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigr=C3=BAn=20Tinna=20Gissurard=C3=B3ttir?= <39527334+sigruntg@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:57:34 +0000 Subject: [PATCH 171/173] feat(register-new-machine): add alert (#16188) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../MachineSection/MachineBasicInformation.ts | 10 ++++++++++ .../register-new-machine/src/lib/messages/machine.ts | 11 +++++++++++ .../aosh/register-new-machine/src/utils/index.ts | 1 + .../register-new-machine/src/utils/isNotCEmarked.ts | 12 ++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 libs/application/templates/aosh/register-new-machine/src/utils/isNotCEmarked.ts diff --git a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts index 0bfe501432a7..22afa79349c9 100644 --- a/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts +++ b/libs/application/templates/aosh/register-new-machine/src/forms/RegisterMachineForm/MachineSection/MachineBasicInformation.ts @@ -1,6 +1,7 @@ import { NO, YES, + buildAlertMessageField, buildCustomField, buildDescriptionField, buildMultiField, @@ -11,6 +12,8 @@ import { import { information, machine } from '../../../lib/messages' import { NEW, USED } from '../../../shared/types' import { getAllCountryCodes } from '@island.is/shared/utils' +import { isNotCEmarked } from '../../../utils' +import { FormValue } from '@island.is/application/types' export const MachineBasicInformation = buildSubSection({ id: 'machineBasicInformation', @@ -131,6 +134,13 @@ export const MachineBasicInformation = buildSubSection({ width: 'half', maxLength: 50, }), + buildAlertMessageField({ + id: 'machine.basicInformation.alert', + title: machine.labels.basicMachineInformation.alertTitle, + message: machine.labels.basicMachineInformation.alertMessage, + alertType: 'warning', + condition: (answer: FormValue) => isNotCEmarked(answer), + }), ], }), ], diff --git a/libs/application/templates/aosh/register-new-machine/src/lib/messages/machine.ts b/libs/application/templates/aosh/register-new-machine/src/lib/messages/machine.ts index 1e9c5992949e..c9f442d38ac4 100644 --- a/libs/application/templates/aosh/register-new-machine/src/lib/messages/machine.ts +++ b/libs/application/templates/aosh/register-new-machine/src/lib/messages/machine.ts @@ -168,6 +168,17 @@ export const machine = { defaultMessage: 'Farmskrárnúmer', description: `Basic machine information cargo file number label`, }, + alertTitle: { + id: 'aosh.rnm.application:machine.labels.basicMachineInformation.alertTitle', + defaultMessage: 'CE merking', + description: `Basic machine information alert title`, + }, + alertMessage: { + id: 'aosh.rnm.application:machine.labels.basicMachineInformation.alertMessage', + defaultMessage: + 'Ef tæki er ekki CE merkt þarf að skila inn samræmisyfirlýsingu og fleiri viðbótargögnum.', + description: `Basic machine information alert message`, + }, }), technicalMachineInformation: defineMessages({ sectionTitle: { diff --git a/libs/application/templates/aosh/register-new-machine/src/utils/index.ts b/libs/application/templates/aosh/register-new-machine/src/utils/index.ts index cf31e24a55c7..fdfad9f918e5 100644 --- a/libs/application/templates/aosh/register-new-machine/src/utils/index.ts +++ b/libs/application/templates/aosh/register-new-machine/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './formatPhoneNumber' export * from './formatDate' export * from './doOwnerAndImporterHaveSameNationalId' export * from './doOwnerAndOperatorHaveSameNationalId' +export * from './isNotCEmarked' diff --git a/libs/application/templates/aosh/register-new-machine/src/utils/isNotCEmarked.ts b/libs/application/templates/aosh/register-new-machine/src/utils/isNotCEmarked.ts new file mode 100644 index 000000000000..0d18b63c878a --- /dev/null +++ b/libs/application/templates/aosh/register-new-machine/src/utils/isNotCEmarked.ts @@ -0,0 +1,12 @@ +import { getValueViaPath } from '@island.is/application/core' +import { FormValue, NO, YES } from '@island.is/application/types' + +export const isNotCEmarked = (answers: FormValue) => { + const markedCE = getValueViaPath( + answers, + 'machine.basicInformation.markedCE', + YES, + ) as typeof NO | typeof YES + + return markedCE === NO +} From 211c5b5fb59731bfbaefd17af00f8d7cfed2547e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:15:09 +0000 Subject: [PATCH 172/173] fix(web): Explicitly pass down elastic index (#16199) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/content-search-indexer/src/lib/indexing.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/content-search-indexer/src/lib/indexing.service.ts b/libs/content-search-indexer/src/lib/indexing.service.ts index ee38d07c8ea6..16be829458a5 100644 --- a/libs/content-search-indexer/src/lib/indexing.service.ts +++ b/libs/content-search-indexer/src/lib/indexing.service.ts @@ -72,6 +72,7 @@ export class IndexingService { while (initialFetch || nextPageToken) { const importerResponse = await importer.doSync({ ...options, + elasticIndex, nextPageToken, folderHash: postSyncOptions?.folderHash, }) From cb65af364c359f90d83551a6da4d24cd40733175 Mon Sep 17 00:00:00 2001 From: valurefugl <65780958+valurefugl@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:21:08 +0100 Subject: [PATCH 173/173] fix(auth-api): Fix url for syslumenn api (#16193) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/services/auth/admin-api/infra/auth-admin-api.ts | 2 +- .../auth/delegation-api/infra/delegation-api.ts | 2 +- apps/services/auth/ids-api/infra/ids-api.ts | 2 +- .../infra/personal-representative.ts | 2 +- apps/services/auth/public-api/infra/auth-public-api.ts | 2 +- charts/identity-server/values.prod.yaml | 10 +++++----- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index fff13b8131cc..376206480242 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -62,7 +62,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { SYSLUMENN_HOST: { dev: 'https://api.syslumenn.is/staging', staging: 'https://api.syslumenn.is/staging', - prod: 'https://api.syslumenn.is', + prod: 'https://api.syslumenn.is/api', }, SYSLUMENN_TIMEOUT: '3000', ZENDESK_CONTACT_FORM_SUBDOMAIN: { diff --git a/apps/services/auth/delegation-api/infra/delegation-api.ts b/apps/services/auth/delegation-api/infra/delegation-api.ts index 60202a6822d8..eea421e6e077 100644 --- a/apps/services/auth/delegation-api/infra/delegation-api.ts +++ b/apps/services/auth/delegation-api/infra/delegation-api.ts @@ -57,7 +57,7 @@ export const serviceSetup = (services: { SYSLUMENN_HOST: { dev: 'https://api.syslumenn.is/staging', staging: 'https://api.syslumenn.is/staging', - prod: 'https://api.syslumenn.is', + prod: 'https://api.syslumenn.is/api', }, SYSLUMENN_TIMEOUT: '3000', }) diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts index e72a270d0d37..14ac4f6c60bf 100644 --- a/apps/services/auth/ids-api/infra/ids-api.ts +++ b/apps/services/auth/ids-api/infra/ids-api.ts @@ -86,7 +86,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => { SYSLUMENN_HOST: { dev: 'https://api.syslumenn.is/staging', staging: 'https://api.syslumenn.is/staging', - prod: 'https://api.syslumenn.is', + prod: 'https://api.syslumenn.is/api', }, SYSLUMENN_TIMEOUT: '3000', }) diff --git a/apps/services/auth/personal-representative/infra/personal-representative.ts b/apps/services/auth/personal-representative/infra/personal-representative.ts index bb3c79004506..6f10744c5a99 100644 --- a/apps/services/auth/personal-representative/infra/personal-representative.ts +++ b/apps/services/auth/personal-representative/infra/personal-representative.ts @@ -45,7 +45,7 @@ export const serviceSetup = SYSLUMENN_HOST: { dev: 'https://api.syslumenn.is/staging', staging: 'https://api.syslumenn.is/staging', - prod: 'https://api.syslumenn.is', + prod: 'https://api.syslumenn.is/api', }, SYSLUMENN_TIMEOUT: '3000', }) diff --git a/apps/services/auth/public-api/infra/auth-public-api.ts b/apps/services/auth/public-api/infra/auth-public-api.ts index 28e81a88467a..3108257b6d75 100644 --- a/apps/services/auth/public-api/infra/auth-public-api.ts +++ b/apps/services/auth/public-api/infra/auth-public-api.ts @@ -66,7 +66,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-public-api'> => { SYSLUMENN_HOST: { dev: 'https://api.syslumenn.is/staging', staging: 'https://api.syslumenn.is/staging', - prod: 'https://api.syslumenn.is', + prod: 'https://api.syslumenn.is/api', }, SYSLUMENN_TIMEOUT: '3000', }) diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 1c3c4d8a443b..077392613786 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -222,7 +222,7 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_HOST: 'https://api.syslumenn.is/api' SYSLUMENN_TIMEOUT: '3000' XROAD_BASE_PATH: 'http://securityserver.island.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS' @@ -312,7 +312,7 @@ services-auth-delegation-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_HOST: 'https://api.syslumenn.is/api' SYSLUMENN_TIMEOUT: '3000' USER_NOTIFICATION_API_URL: 'https://user-notification.internal.island.is' XROAD_BASE_PATH: 'http://securityserver.island.is' @@ -412,7 +412,7 @@ services-auth-ids-api: PUBLIC_URL: 'https://innskra.island.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_HOST: 'https://api.syslumenn.is/api' SYSLUMENN_TIMEOUT: '3000' USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]' USER_PROFILE_CLIENT_URL: 'https://service-portal-api.internal.island.is' @@ -586,7 +586,7 @@ services-auth-personal-representative: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_HOST: 'https://api.syslumenn.is/api' SYSLUMENN_TIMEOUT: '3000' XROAD_BASE_PATH: 'http://securityserver.island.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS' @@ -743,7 +743,7 @@ services-auth-public-api: PUBLIC_URL: 'https://innskra.island.is/api' REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - SYSLUMENN_HOST: 'https://api.syslumenn.is' + SYSLUMENN_HOST: 'https://api.syslumenn.is/api' SYSLUMENN_TIMEOUT: '3000' XROAD_BASE_PATH: 'http://securityserver.island.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS'