diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a740b9f7fb3c..341480407d13 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -159,6 +159,7 @@ codemagic.yaml /libs/api/domains/official-journal-of-iceland-application/ @island-is/hugsmidjan /libs/api/domains/document-provider/ @island-is/hugsmidjan @island-is/core /libs/api/domains/housing-benefits/ @island-is/hugsmidjan +/libs/api/domains/law-and-order/ @island-is/hugsmidjan /libs/clients/documents/ @island-is/hugsmidjan /libs/clients/documents-v2/ @island-is/hugsmidjan /libs/clients/finance/ @island-is/hugsmidjan @@ -198,6 +199,7 @@ codemagic.yaml /libs/portals/admin/air-discount-scheme @island-is/hugsmidjan /libs/application/templates/official-journal-of-iceland/ @island-is/hugsmidjan /libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/ @island-is/hugsmidjan +/libs/clients/judicial-system-sp/ @island-is/hugsmidjan /libs/application/templates/data-protection-complaint/ @island-is/norda /libs/application/templates/institution-collaboration/ @island-is/norda @island-is/fuglar /libs/application/templates/login-service/ @island-is/norda diff --git a/.github/workflows/config-values.yaml b/.github/workflows/config-values.yaml index 12a6369c0696..883861366f8b 100644 --- a/.github/workflows/config-values.yaml +++ b/.github/workflows/config-values.yaml @@ -140,7 +140,7 @@ jobs: working-directory: infra run: node -r esbuild-register src/secrets.ts get-all-required-secrets --env=${{ matrix.env }} >> LOCAL_SECRETS - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.DESCRIBE_SSM_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DESCRIBE_SSM_AWS_SECRET_ACCESS_KEY }} @@ -156,7 +156,7 @@ jobs: working-directory: infra - name: Configure AWS Credentials for IDS Prod - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.DESCRIBE_SSM_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.DESCRIBE_SSM_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index aef35c5de860..51e54f62e853 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -284,13 +284,14 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v4 - - name: Run ShellCheck - uses: ludeeus/action-shellcheck@2.0.0 + - uses: reviewdog/action-shellcheck@v1 with: - ignore_paths: >- - node_modules - apps/native/app/android - severity: warning + github_token: ${{ secrets.github_token }} + reporter: github-pr-review + fail_on_error: true + level: info + exclude: >- + */node_modules/* formatting: needs: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 490f7cb1abb9..a442c1f6573e 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -603,7 +603,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }} - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }} @@ -664,7 +664,7 @@ jobs: run: '[[ ${{ needs.docker-build.result }} != "failure" ]] || exit 1' - uses: actions/checkout@v3 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.prettierignore b/.prettierignore index bd39c451491d..2619b48cd441 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,4 +13,5 @@ .yarn /infra/helm/ /.nx/cache -/.nx/workspace-data \ No newline at end of file +/.nx/workspace-data +apps/web/public/assets/pdf.worker.min.mjs \ No newline at end of file diff --git a/apps/api/infra/api.ts b/apps/api/infra/api.ts index ca9a1b261d2d..cc8308e42a37 100644 --- a/apps/api/infra/api.ts +++ b/apps/api/infra/api.ts @@ -46,6 +46,7 @@ import { UniversityCareers, OfficialJournalOfIceland, OfficialJournalOfIcelandApplication, + JudicialSystemServicePortal, Frigg, HealthDirectorateOrganDonation, HealthDirectorateVaccination, @@ -385,6 +386,8 @@ export const serviceSetup = (services: { ULTRAVIOLET_RADIATION_API_KEY: '/k8s/api/ULTRAVIOLET_RADIATION_API_KEY', UMBODSMADUR_SKULDARA_COST_OF_LIVING_CALCULATOR_API_URL: '/k8s/api/UMBODSMADUR_SKULDARA_COST_OF_LIVING_CALCULATOR_API_URL', + VINNUEFTIRLITID_CAMPAIGN_MONITOR_API_KEY: + '/k8s/api/VINNUEFTIRLITID_CAMPAIGN_MONITOR_API_KEY', }) .xroad( AdrAndMachine, @@ -432,6 +435,7 @@ export const serviceSetup = (services: { SignatureCollection, SocialInsuranceAdministration, OfficialJournalOfIceland, + JudicialSystemServicePortal, OfficialJournalOfIcelandApplication, Frigg, HealthDirectorateOrganDonation, diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 885e71d4dc1f..71017ebdd1a2 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -29,6 +29,7 @@ import { ElectronicRegistrationsModule } from '@island.is/api/domains/electronic import { EmailSignupModule, ZenterSignupConfig, + CampaignMonitorSignupConfig, } from '@island.is/api/domains/email-signup' import { EndorsementSystemModule } from '@island.is/api/domains/endorsement-system' import { EnergyFundsServiceModule } from '@island.is/api/domains/energy-funds' @@ -188,7 +189,9 @@ import { } from '@island.is/clients/university-careers' import { HousingBenefitsConfig } from '@island.is/clients/hms-housing-benefits' import { UserProfileClientConfig } from '@island.is/clients/user-profile' +import { LawAndOrderModule } from '@island.is/api/domains/law-and-order' import { UltravioletRadiationClientConfig } from '@island.is/clients/ultraviolet-radiation' +import { JudicialSystemSPClientConfig } from '@island.is/clients/judicial-system-sp' import { CriminalRecordClientConfig } from '@island.is/clients/criminal-record' import { HealthInsuranceV2ClientConfig } from '@island.is/clients/icelandic-health-insurance/health-insurance' import { VmstClientConfig } from '@island.is/clients/vmst' @@ -328,6 +331,7 @@ const environment = getConfig AuthAdminModule, HousingBenefitCalculatorModule, SignatureCollectionModule, + LawAndOrderModule, UmbodsmadurSkuldaraModule, HealthDirectorateModule, ConfigModule.forRoot({ @@ -387,6 +391,7 @@ const environment = getConfig DocumentClientConfig, DocumentsClientV2Config, ZenterSignupConfig, + CampaignMonitorSignupConfig, PaymentScheduleClientConfig, JudicialAdministrationClientConfig, CommunicationsConfig, @@ -419,11 +424,12 @@ const environment = getConfig LicenseConfig, UserProfileClientConfig, UltravioletRadiationClientConfig, + JudicialSystemSPClientConfig, FriggClientConfig, GradeClientConfig, VmstClientConfig, - HealthInsuranceV2ClientConfig, CriminalRecordClientConfig, + HealthInsuranceV2ClientConfig, UmbodsmadurSkuldaraClientConfig, emailModuleConfig, ], diff --git a/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts b/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts index c05354ab4f1f..2eafaf9a4fc3 100644 --- a/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts +++ b/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts @@ -1,6 +1,7 @@ import { getApplicantName, getApplicationNameTranslationString, + getApplicationStatisticsNameTranslationString, getPaymentStatusForAdmin, } from './application' import { @@ -8,107 +9,191 @@ import { createApplicationTemplate, } from '@island.is/application/testing' -describe('Testing utility functions for applicatios', () => { - it('Should return unpaid when not fulfilled and payment created date defined', () => { - expect( - getPaymentStatusForAdmin({ fulfilled: false, created: new Date() }), - ).toEqual('unpaid') - }) +describe('Testing utility functions for applications', () => { + describe('getPaymentStatusForAdmin', () => { + it('Should return unpaid when not fulfilled and payment created date defined', () => { + expect( + getPaymentStatusForAdmin({ fulfilled: false, created: new Date() }), + ).toEqual('unpaid') + }) - it('Should return paid when fulfilled and payment created date defined', () => { - expect( - getPaymentStatusForAdmin({ fulfilled: true, created: new Date() }), - ).toEqual('paid') - }) + it('Should return paid when fulfilled and payment created date defined', () => { + expect( + getPaymentStatusForAdmin({ fulfilled: true, created: new Date() }), + ).toEqual('paid') + }) - it('Should return null when payment object is not defined', () => { - expect(getPaymentStatusForAdmin(null)).toEqual(null) + it('Should return null when payment object is not defined', () => { + expect(getPaymentStatusForAdmin(null)).toEqual(null) + }) }) - it('Should return the applicant name when nationalRegistry has a fullName', () => { - expect( - getApplicantName( - createApplication({ - externalData: { - nationalRegistry: { - data: { - fullName: 'Test User', + describe('getApplicantName', () => { + it('Should return the applicant name when nationalRegistry has a fullName', () => { + expect( + getApplicantName( + createApplication({ + externalData: { + nationalRegistry: { + data: { + fullName: 'Test User', + }, + date: new Date(), + status: 'success', }, - date: new Date(), - status: 'success', }, - }, - }), - ), - ).toEqual('Test User') - }) + }), + ), + ).toEqual('Test User') + }) + + it('Should return name of the applicant when identity external data is defined', () => { + expect( + getApplicantName( + createApplication({ + externalData: { + identity: { + data: { + name: 'Test User', + }, + date: new Date(), + status: 'success', + }, + }, + }), + ), + ).toEqual('Test User') + }) - it('Should return name of the applicantt when identity external data is defined', () => { - expect( - getApplicantName( - createApplication({ - externalData: { - identity: { - data: { - name: 'Test User', + it('Should return name of the applicant when person external data is defined', () => { + expect( + getApplicantName( + createApplication({ + externalData: { + person: { + data: { + fullname: 'Test User', + }, + date: new Date(), + status: 'success', }, - date: new Date(), - status: 'success', }, - }, - }), - ), - ).toEqual('Test User') + }), + ), + ).toEqual('Test User') + }) + + it('Should return null when no external data is defined', () => { + expect( + getApplicantName( + createApplication({ + externalData: {}, + }), + ), + ).toBeNull() + }) }) -}) -it('Should return the name of the application when defined with a string', () => { - expect( - getApplicationNameTranslationString( - createApplicationTemplate(), - createApplication({ - externalData: { - identity: { - data: { - name: 'Test User', + describe('getApplicationNameTranslationString', () => { + it('Should return the name of the application when defined with a string', () => { + expect( + getApplicationNameTranslationString( + createApplicationTemplate(), + createApplication({ + externalData: { + identity: { + data: { + name: 'Test User', + }, + date: new Date(), + status: 'success', + }, }, - date: new Date(), - status: 'success', - }, - }, - }), - (message) => message as any, - ), - ).toEqual('Test application') -}) + }), + (message) => message as any, + ), + ).toEqual('Test application') + }) -it('Should return name of the application according to applicant age', () => { - expect( - getApplicationNameTranslationString( - createApplicationTemplate({ - name: (application) => - Number(application.answers.age) >= 20 - ? 'Adult Application' - : 'Child Application', - }), - createApplication({ - answers: { - age: 20, - }, - }), - (message) => message as any, - ), - ).toEqual('Adult Application') -}) + it('Should return the correct application name based on a predefined age threshold', () => { + expect( + getApplicationNameTranslationString( + createApplicationTemplate({ + name: (application) => + Number(application.answers.age) >= 20 + ? 'Adult Application' + : 'Child Application', + }), + createApplication({ + answers: { + age: 20, + }, + }), + (message) => message as any, + ), + ).toEqual('Adult Application') + }) -it('Should return name of the application according to applicant age', () => { - expect( - getApplicationNameTranslationString( - createApplicationTemplate({ - name: 'Normal Application', - }), - createApplication(), - (message) => message as any, - ), - ).toEqual('Normal Application') + it('Should return the name of the application when defined with a static string', () => { + expect( + getApplicationNameTranslationString( + createApplicationTemplate({ + name: 'Normal Application', + }), + createApplication(), + (message) => message as any, + ), + ).toEqual('Normal Application') + }) + + describe('getApplicationStatisticsNameTranslationString', () => { + const applicationStatistics = { + typeid: 'test', + count: 1, + draft: 1, + inprogress: 1, + completed: 1, + rejected: 1, + approved: 1, + } + it('Should return the translated name of the application statistics when defined with a string', () => { + expect( + getApplicationStatisticsNameTranslationString( + createApplicationTemplate({ + name: 'test name', + }), + applicationStatistics, + (message) => (message + ' formatted') as string, + ), + ).toEqual('test name formatted') + }) + + it('Should return the translated name of the application statistics when defined with a function', () => { + expect( + getApplicationStatisticsNameTranslationString( + createApplicationTemplate({ + name: (application) => application.typeId + ' name', + }), + applicationStatistics, + (message) => (message + ' formatted') as string, + ), + ).toEqual('test name formatted') + }) + + it('Should return the translated name of the application statistics when defined with a function that returns an object', () => { + expect( + getApplicationStatisticsNameTranslationString( + createApplicationTemplate({ + name: (application) => ({ + name: application.typeId + ' name', + value: '1', + }), + }), + applicationStatistics, + (message, value) => `${message} ${value?.value} formatted`, + ), + ).toEqual('test name 1 formatted') + }) + }) + }) }) diff --git a/apps/application-system/api/src/app/modules/application/utils/delegationUtils.spec.ts b/apps/application-system/api/src/app/modules/application/utils/delegationUtils.spec.ts new file mode 100644 index 000000000000..d23a6dcf950d --- /dev/null +++ b/apps/application-system/api/src/app/modules/application/utils/delegationUtils.spec.ts @@ -0,0 +1,49 @@ +import { createApplication } from '@island.is/application/testing' +import { isNewActor } from './delegationUtils' + +describe('Testing utility functions for delegations', () => { + describe('isNewActor', () => { + it('Should return true when user is not an actor and is the applicant', () => { + const application = createApplication({ + applicant: '1234567890', + applicantActors: [], + }) + const user = { + nationalId: '1234567890', + actor: { + nationalId: '1234567890', + }, + } + + expect(isNewActor(application, user)).toEqual(true) + }) + + it('Should return false when user actor is already in applicantActors', () => { + const application = createApplication({ + applicant: '1234567890', + applicantActors: ['1234567890'], + }) + const user = { + nationalId: '1234567890', + actor: { + nationalId: '1234567890', + }, + } + + expect(isNewActor(application, user)).toEqual(false) + }) + + it('Should return false when user is not the actor', () => { + const application = createApplication({ + applicant: '1234567890', + applicantActors: [], + }) + + const user = { + nationalId: '1234567890', + } + + expect(isNewActor(application, user)).toEqual(false) + }) + }) +}) diff --git a/apps/contentful-apps/components/MideindTranslationSidebar/MideindTranslationSidebar.tsx b/apps/contentful-apps/components/MideindTranslationSidebar/MideindTranslationSidebar.tsx index 60e7f0b7531e..932f28fe3aa5 100644 --- a/apps/contentful-apps/components/MideindTranslationSidebar/MideindTranslationSidebar.tsx +++ b/apps/contentful-apps/components/MideindTranslationSidebar/MideindTranslationSidebar.tsx @@ -153,6 +153,16 @@ export const MideindTranslationSidebar = () => { isDisabled={loading} style={{ width: '100%' }} onClick={async () => { + const shouldContinue = await sdk.dialogs.openConfirm({ + title: 'Are you sure?', + message: + 'All english text fields will be replaced with a "Miðeind" translation', + }) + + if (!shouldContinue) { + return + } + setLoading(true) await handleClick(sdk) setLoading(false) diff --git a/apps/contentful-apps/components/lists/ContentfulField.tsx b/apps/contentful-apps/components/editors/ContentfulField.tsx similarity index 94% rename from apps/contentful-apps/components/lists/ContentfulField.tsx rename to apps/contentful-apps/components/editors/ContentfulField.tsx index 723147b9cfad..031da947f28f 100644 --- a/apps/contentful-apps/components/lists/ContentfulField.tsx +++ b/apps/contentful-apps/components/editors/ContentfulField.tsx @@ -5,14 +5,16 @@ import { Box, FormControl, Text } from '@contentful/f36-components' import { mapLocalesToFieldApis } from './utils' -export const ContentfulField = (props: { +export interface ContentfulFieldProps { sdk: EditorExtensionSDK localeToFieldMapping: Record> - fieldID: keyof typeof props.localeToFieldMapping + fieldID: keyof ContentfulFieldProps['localeToFieldMapping'] displayName: string widgetId?: string helpText?: string -}) => { +} + +export const ContentfulField = (props: ContentfulFieldProps) => { const availableLocales = useMemo(() => { const validLocales = props.sdk.locales.available.filter( (locale) => props.localeToFieldMapping[props.fieldID]?.[locale], diff --git a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx new file mode 100644 index 000000000000..3faf439d0d62 --- /dev/null +++ b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx @@ -0,0 +1,133 @@ +import { useEffect, useMemo, useState } from 'react' +import type { EntryProps, KeyValueMap } from 'contentful-management' +import dynamic from 'next/dynamic' +import type { EditorExtensionSDK } from '@contentful/app-sdk' +import { Box } from '@contentful/f36-components' +import { useCMA, useSDK } from '@contentful/react-apps-toolkit' + +import { mapLocalesToFieldApis } from '../utils' +import { TeamMemberFilterTagsField } from './TeamMemberFilterTagsField' + +const ContentfulField = dynamic( + () => + // Dynamically import via client side rendering since the @contentful/default-field-editors package accesses the window and navigator global objects + import('../ContentfulField').then(({ ContentfulField }) => ContentfulField), + { + ssr: false, + }, +) + +const createField = (name: string, sdk: EditorExtensionSDK) => { + return mapLocalesToFieldApis(sdk.entry.fields[name].locales, sdk, name) +} + +const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { + return { + name: createField('name', sdk), + title: createField('title', sdk), + mynd: createField('mynd', sdk), + imageOnSelect: createField('imageOnSelect', sdk), + filterTags: createField('filterTags', sdk), + email: createField('email', sdk), + phone: createField('phone', sdk), + intro: createField('intro', sdk), + } +} + +export const TeamMemberEditor = () => { + const sdk = useSDK() + const cma = useCMA() + + const localeToFieldMapping = useMemo(() => { + return createLocaleToFieldMapping(sdk) + }, [sdk]) + + const [teamList, setTeamList] = useState>() + + useEffect(() => { + const fetchTeamList = async () => { + const teamLists = await cma.entry.getMany({ + query: { + content_type: 'teamList', + links_to_entry: sdk.entry.getSys().id, + }, + }) + if (teamLists.items.length > 0) setTeamList(teamLists.items[0]) + } + + fetchTeamList() + }, [cma.entry, sdk.entry]) + + const teamListIsAccordionVariant = + teamList?.fields?.variant?.[sdk.locales.default] === 'accordion' + + return ( + + + + + + + + {teamListIsAccordionVariant && ( + + + + + + )} + + + ) +} diff --git a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx new file mode 100644 index 000000000000..2a3994a7fffa --- /dev/null +++ b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx @@ -0,0 +1,189 @@ +import { useEffect, useState } from 'react' +import { useDebounce } from 'react-use' +import { + CollectionProp, + EntryProps, + KeyValueMap, + QueryOptions, + SysLink, +} from 'contentful-management' +import type { CMAClient, EditorExtensionSDK } from '@contentful/app-sdk' +import { Checkbox, Spinner, Stack, Text } from '@contentful/f36-components' +import { useCMA } 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 +} + +interface TeamMemberFilterTagsField { + sdk: EditorExtensionSDK +} + +export const TeamMemberFilterTagsField = ({ + sdk, +}: TeamMemberFilterTagsField) => { + const cma = useCMA() + const [isLoading, setIsLoading] = useState(true) + + const [filterTagSysLinks, setFilterTagSysLinks] = useState( + sdk.entry.fields['filterTags']?.getValue() ?? [], + ) + + const [tagGroups, setTagGroups] = useState< + { + tagGroup: EntryProps + tags: EntryProps[] + }[] + >([]) + + 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.entry.fields['filterTags']?.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]} + + ) + })} + + + ) + })} + + )} + + ) +} diff --git a/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx b/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx similarity index 97% rename from apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx rename to apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx index ec933a66f49d..0740aff1d59d 100644 --- a/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx +++ b/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx @@ -17,7 +17,7 @@ import { import { PlusIcon } from '@contentful/f36-icons' import { useCMA, useSDK } from '@contentful/react-apps-toolkit' -import { mapLocalesToFieldApis } from '../utils' +import { mapLocalesToFieldApis } from '../../utils' const SEARCH_DEBOUNCE_TIME_IN_MS = 300 const LIST_ITEM_CONTENT_TYPE_ID = 'genericListItem' @@ -46,7 +46,9 @@ const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { const ContentfulField = dynamic( () => // Dynamically import via client side rendering since the @contentful/default-field-editors package accesses the window and navigator global objects - import('../ContentfulField').then(({ ContentfulField }) => ContentfulField), + import('../../ContentfulField').then( + ({ ContentfulField }) => ContentfulField, + ), { ssr: false, }, diff --git a/apps/contentful-apps/components/lists/GenericListItemEditor/GenericListItemEditor.tsx b/apps/contentful-apps/components/editors/lists/GenericListItemEditor/GenericListItemEditor.tsx similarity index 93% rename from apps/contentful-apps/components/lists/GenericListItemEditor/GenericListItemEditor.tsx rename to apps/contentful-apps/components/editors/lists/GenericListItemEditor/GenericListItemEditor.tsx index c5bd0b45d6d9..270660b6d17e 100644 --- a/apps/contentful-apps/components/lists/GenericListItemEditor/GenericListItemEditor.tsx +++ b/apps/contentful-apps/components/editors/lists/GenericListItemEditor/GenericListItemEditor.tsx @@ -4,7 +4,7 @@ import { EditorExtensionSDK } from '@contentful/app-sdk' import { Box } from '@contentful/f36-components' import { useSDK } from '@contentful/react-apps-toolkit' -import { mapLocalesToFieldApis } from '../utils' +import { mapLocalesToFieldApis } from '../../utils' const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { return { @@ -26,7 +26,9 @@ const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { const ContentfulField = dynamic( () => // Dynamically import via client side rendering since the @contentful/default-field-editors package accesses the window and navigator global objects - import('../ContentfulField').then(({ ContentfulField }) => ContentfulField), + import('../../ContentfulField').then( + ({ ContentfulField }) => ContentfulField, + ), { ssr: false, }, diff --git a/apps/contentful-apps/components/lists/utils.ts b/apps/contentful-apps/components/editors/utils.ts similarity index 100% rename from apps/contentful-apps/components/lists/utils.ts rename to apps/contentful-apps/components/editors/utils.ts diff --git a/apps/contentful-apps/pages/editors/generic-list-editor.ts b/apps/contentful-apps/pages/editors/generic-list-editor.ts index a701c779c83e..5be02cb3ca50 100644 --- a/apps/contentful-apps/pages/editors/generic-list-editor.ts +++ b/apps/contentful-apps/pages/editors/generic-list-editor.ts @@ -1,3 +1,3 @@ -import { GenericListEditor } from '../../components/lists/GenericListEditor/GenericListEditor' +import { GenericListEditor } from '../../components/editors/lists/GenericListEditor/GenericListEditor' export default GenericListEditor diff --git a/apps/contentful-apps/pages/editors/generic-list-item-editor.ts b/apps/contentful-apps/pages/editors/generic-list-item-editor.ts index f59cc2153543..e499a338b821 100644 --- a/apps/contentful-apps/pages/editors/generic-list-item-editor.ts +++ b/apps/contentful-apps/pages/editors/generic-list-item-editor.ts @@ -1,3 +1,3 @@ -import { GenericListItemEditor } from '../../components/lists/GenericListItemEditor/GenericListItemEditor' +import { GenericListItemEditor } from '../../components/editors/lists/GenericListItemEditor/GenericListItemEditor' export default GenericListItemEditor diff --git a/apps/contentful-apps/pages/editors/team-member-editor.ts b/apps/contentful-apps/pages/editors/team-member-editor.ts new file mode 100644 index 000000000000..457e78592a66 --- /dev/null +++ b/apps/contentful-apps/pages/editors/team-member-editor.ts @@ -0,0 +1,3 @@ +import { TeamMemberEditor } from '../../components/editors/TeamMemberEditor/TeamMemberEditor' + +export default TeamMemberEditor diff --git a/apps/download-service/src/app/modules/documents/document.controller.ts b/apps/download-service/src/app/modules/documents/document.controller.ts index 215d00c93333..1d9fd63788de 100644 --- a/apps/download-service/src/app/modules/documents/document.controller.ts +++ b/apps/download-service/src/app/modules/documents/document.controller.ts @@ -1,17 +1,3 @@ -import { - Body, - Controller, - Header, - Post, - Res, - Param, - UseGuards, -} from '@nestjs/common' -import { ApiOkResponse, ApiTags } from '@nestjs/swagger' -import { GetDocumentDto } from './dto/getDocument.dto' -import { Response } from 'express' -import { DocumentClient } from '@island.is/clients/documents' -import { DocumentsScope } from '@island.is/auth/scopes' import type { User } from '@island.is/auth-nest-tools' import { CurrentUser, @@ -19,7 +5,12 @@ import { Scopes, ScopesGuard, } from '@island.is/auth-nest-tools' +import { DocumentsScope } from '@island.is/auth/scopes' +import { DocumentClient } from '@island.is/clients/documents' import { AuditService } from '@island.is/nest/audit' +import { Controller, Header, Param, Post, Res, UseGuards } from '@nestjs/common' +import { ApiOkResponse, ApiTags } from '@nestjs/swagger' +import { Response } from 'express' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(DocumentsScope.main) @@ -40,7 +31,6 @@ export class DocumentController { async getPdf( @Param('pdfId') pdfId: string, @CurrentUser() user: User, - @Body() resource: GetDocumentDto, @Res() res: Response, ) { const rawDocumentDTO = await this.documentClient.customersDocument({ diff --git a/apps/download-service/src/app/modules/documents/dto/getDocument.dto.ts b/apps/download-service/src/app/modules/documents/dto/getDocument.dto.ts deleted file mode 100644 index 8064107f4a18..000000000000 --- a/apps/download-service/src/app/modules/documents/dto/getDocument.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString, IsJWT } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class GetDocumentDto { - @IsString() - @ApiProperty() - readonly documentId!: string - - @IsJWT() - @ApiProperty() - readonly __accessToken!: string -} diff --git a/apps/download-service/src/app/modules/education-documents/dto/getEducationGraduationDocument.ts b/apps/download-service/src/app/modules/education-documents/dto/getEducationGraduationDocument.ts deleted file mode 100644 index def062659eb8..000000000000 --- a/apps/download-service/src/app/modules/education-documents/dto/getEducationGraduationDocument.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsJWT } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class GetEducationGraduationDocumentDto { - @IsJWT() - @ApiProperty() - readonly __accessToken!: string -} diff --git a/apps/download-service/src/app/modules/education-documents/education-document.controller.ts b/apps/download-service/src/app/modules/education-documents/education-document.controller.ts index 516ad2cdcdac..0cba1ec0a54a 100644 --- a/apps/download-service/src/app/modules/education-documents/education-document.controller.ts +++ b/apps/download-service/src/app/modules/education-documents/education-document.controller.ts @@ -1,15 +1,3 @@ -import { - Body, - Controller, - Header, - Post, - Res, - Param, - UseGuards, -} from '@nestjs/common' -import { ApiOkResponse } from '@nestjs/swagger' -import { Response } from 'express' -import { ApiScope } from '@island.is/auth/scopes' import type { User } from '@island.is/auth-nest-tools' import { CurrentUser, @@ -17,14 +5,17 @@ import { Scopes, ScopesGuard, } from '@island.is/auth-nest-tools' -import { AuditService } from '@island.is/nest/audit' -import { GetEducationGraduationDocumentDto } from './dto/getEducationGraduationDocument' +import { ApiScope } from '@island.is/auth/scopes' import { UniversityCareersClientService, UniversityIdShort, + UniversityShortIdMap, } from '@island.is/clients/university-careers' +import { AuditService } from '@island.is/nest/audit' import { Locale } from '@island.is/shared/types' -import { UniversityShortIdMap } from '@island.is/clients/university-careers' +import { Controller, Header, Param, Post, Res, UseGuards } from '@nestjs/common' +import { ApiOkResponse } from '@nestjs/swagger' +import { Response } from 'express' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(ApiScope.education) @@ -48,16 +39,10 @@ export class EducationController { @Param('university') uni: UniversityIdShort, @CurrentUser() user: User, - @Body() resource: GetEducationGraduationDocumentDto, @Res() res: Response, ) { - const authUser: User = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } - const documentResponse = await this.universitiesApi.getStudentTrackPdf( - authUser, + user, parseInt(trackNumber), UniversityShortIdMap[uni], lang as Locale, @@ -78,10 +63,10 @@ export class EducationController { 'Content-Disposition', `inline; filename=${user.nationalId}-skoli-${UniversityShortIdMap[uni]}-brautskraning-${trackNumber}.pdf`, ) - res.header('Content-Type: application/pdf') - res.header('Pragma: no-cache') - res.header('Cache-Control: no-cache') - res.header('Cache-Control: nmax-age=0') + res.header('Content-Type', 'application/pdf') + res.header('Pragma', 'no-cache') + res.header('Cache-Control', 'no-cache') + res.header('Cache-Control', 'nmax-age=0') return res.status(200).end(buffer) } return res.end() diff --git a/apps/download-service/src/app/modules/finance-documents/document.controller.ts b/apps/download-service/src/app/modules/finance-documents/document.controller.ts index 3b32a9b044f7..23b15bac679d 100644 --- a/apps/download-service/src/app/modules/finance-documents/document.controller.ts +++ b/apps/download-service/src/app/modules/finance-documents/document.controller.ts @@ -1,25 +1,24 @@ +import type { User } from '@island.is/auth-nest-tools' +import { + CurrentUser, + IdsUserGuard, + Scopes, + ScopesGuard, +} from '@island.is/auth-nest-tools' +import { ApiScope } from '@island.is/auth/scopes' +import { FinanceClientService } from '@island.is/clients/finance' +import { AuditService } from '@island.is/nest/audit' import { Body, Controller, Header, + Param, Post, Res, - Param, UseGuards, } from '@nestjs/common' import { ApiOkResponse } from '@nestjs/swagger' import { Response } from 'express' -import { FinanceClientService } from '@island.is/clients/finance' -import { ApiScope } from '@island.is/auth/scopes' -import type { User } from '@island.is/auth-nest-tools' -import { - CurrentUser, - IdsUserGuard, - Scopes, - ScopesGuard, -} from '@island.is/auth-nest-tools' -import { AuditService } from '@island.is/nest/audit' -import { GetFinanceDocumentDto } from './dto/getFinanceDocument.dto' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(ApiScope.financeOverview, ApiScope.financeSalary) @@ -37,28 +36,25 @@ export class FinanceDocumentController { description: 'Get a PDF document from the Finance service', }) async getFinancePdf( - @Param('pdfId') pdfId: string, @CurrentUser() user: User, - @Body() resource: GetFinanceDocumentDto, @Res() res: Response, + @Param('pdfId') pdfId: string, + @Body('annualDoc') annualDoc?: string, ) { - const authUser: User = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } - const documentResponse = resource.annualDoc + const documentResponse = annualDoc ? await this.financeService.getAnnualStatusDocument( user.nationalId, pdfId, - authUser, + user, ) : await this.financeService.getFinanceDocument( user.nationalId, pdfId, - authUser, + user, ) const documentBase64 = documentResponse?.docment?.document + if (documentBase64) { this.auditService.audit({ action: 'getFinancePdf', @@ -70,9 +66,9 @@ export class FinanceDocumentController { res.header('Content-length', buffer.length.toString()) res.header('Content-Disposition', `inline; filename=${pdfId}.pdf`) - res.header('Pragma: no-cache') - res.header('Cache-Control: no-cache') - res.header('Cache-Control: nmax-age=0') + res.header('Pragma', 'no-cache') + res.header('Cache-Control', 'no-cache') + res.header('Cache-Control', 'nmax-age=0') return res.status(200).end(buffer) } diff --git a/apps/download-service/src/app/modules/finance-documents/dto/getFinanceDocument.dto.ts b/apps/download-service/src/app/modules/finance-documents/dto/getFinanceDocument.dto.ts deleted file mode 100644 index bd6ffaca8d8a..000000000000 --- a/apps/download-service/src/app/modules/finance-documents/dto/getFinanceDocument.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsJWT, IsString, IsOptional } from 'class-validator' -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' - -export class GetFinanceDocumentDto { - @IsJWT() - @ApiProperty() - readonly __accessToken!: string - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly annualDoc: string | undefined -} diff --git a/apps/download-service/src/app/modules/health/dto/getHealthPaymentDocument.dto.ts b/apps/download-service/src/app/modules/health/dto/getHealthPaymentDocument.dto.ts deleted file mode 100644 index 8318e3e1ce3a..000000000000 --- a/apps/download-service/src/app/modules/health/dto/getHealthPaymentDocument.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsJWT } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class GetGetHealthPaymentDocumentDto { - @IsJWT() - @ApiProperty() - readonly __accessToken!: string -} diff --git a/apps/download-service/src/app/modules/health/payment-overview-documents.controller.ts b/apps/download-service/src/app/modules/health/payment-overview-documents.controller.ts index 34351ac29d47..1bb2aa7e9701 100644 --- a/apps/download-service/src/app/modules/health/payment-overview-documents.controller.ts +++ b/apps/download-service/src/app/modules/health/payment-overview-documents.controller.ts @@ -1,16 +1,3 @@ -import { - Controller, - Header, - Post, - Res, - Param, - UseGuards, - Inject, - Body, -} from '@nestjs/common' -import { ApiOkResponse } from '@nestjs/swagger' -import { Response } from 'express' -import { ApiScope } from '@island.is/auth/scopes' import type { User } from '@island.is/auth-nest-tools' import { AuthMiddleware, @@ -19,9 +6,12 @@ import { Scopes, ScopesGuard, } from '@island.is/auth-nest-tools' -import { AuditService } from '@island.is/nest/audit' +import { ApiScope } from '@island.is/auth/scopes' import { PaymentsOverviewApi } from '@island.is/clients/icelandic-health-insurance/rights-portal' -import { GetGetHealthPaymentDocumentDto } from './dto/getHealthPaymentDocument.dto' +import { AuditService } from '@island.is/nest/audit' +import { Controller, Header, Param, Post, Res, UseGuards } from '@nestjs/common' +import { ApiOkResponse } from '@nestjs/swagger' +import { Response } from 'express' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(ApiScope.healthPayments) @@ -42,16 +32,10 @@ export class HealthPaymentsOverviewController { async getHealthPaymentOverviewPdf( @Param('documentId') documentId: string, @CurrentUser() user: User, - @Body() resource: GetGetHealthPaymentDocumentDto, @Res() res: Response, ) { - const authUser = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } - const documentResponse = await this.paymentApi - .withMiddleware(new AuthMiddleware(authUser)) + .withMiddleware(new AuthMiddleware(user)) .getPaymentsOverviewDocument({ documentId: parseInt(documentId), }) @@ -73,19 +57,15 @@ export class HealthPaymentsOverviewController { const buffer = Buffer.from(documentResponse.data, 'base64') - // const contentArrayBuffer = - // await documentResponse.contentType.arrayBuffer() - // const buffer = Buffer.from(contentArrayBuffer) - res.header('Content-length', buffer.length.toString()) res.header( 'Content-Disposition', `inline; filename=${user.nationalId}-health-payment-overview-${documentResponse.fileName}.pdf`, ) - res.header('Content-Type: application/pdf') - res.header('Pragma: no-cache') - res.header('Cache-Control: no-cache') - res.header('Cache-Control: nmax-age=0') + res.header('Content-Type', 'application/pdf') + res.header('Pragma', 'no-cache') + res.header('Cache-Control', 'no-cache') + res.header('Cache-Control', 'nmax-age=0') return res.status(200).end(buffer) } return res.end() diff --git a/apps/download-service/src/app/modules/occupational-licenses/occupational-license.controller.ts b/apps/download-service/src/app/modules/occupational-licenses/occupational-license.controller.ts index 7f073fe8b462..d5fb26bb7b7f 100644 --- a/apps/download-service/src/app/modules/occupational-licenses/occupational-license.controller.ts +++ b/apps/download-service/src/app/modules/occupational-licenses/occupational-license.controller.ts @@ -11,14 +11,12 @@ import { } from '@island.is/auth-nest-tools' import { AuditService } from '@island.is/nest/audit' import { MMSApi } from '@island.is/clients/mms' -import { DistrictCommissionersLicensesService } from '@island.is/clients/district-commissioners-licenses' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(ApiScope.education) @Controller('occupational-licenses') export class OccupationalLicensesController { constructor( - private readonly dcApi: DistrictCommissionersLicensesService, private readonly mmsApi: MMSApi, private readonly auditService: AuditService, ) {} @@ -55,10 +53,10 @@ export class OccupationalLicensesController { 'Content-Disposition', `inline; filename=${user.nationalId}-starfsleyfi-${licenceId}.pdf`, ) - res.header('Content-Type: application/pdf') - res.header('Pragma: no-cache') - res.header('Cache-Control: no-cache') - res.header('Cache-Control: nmax-age=0') + res.header('Content-Type', 'application/pdf') + res.header('Pragma', 'no-cache') + res.header('Cache-Control', 'no-cache') + res.header('Cache-Control', 'nmax-age=0') return res.status(200).end(buffer) } return res.end() diff --git a/apps/download-service/src/app/modules/regulation-documents/dto/getRegulationDraftDocument.dto.ts b/apps/download-service/src/app/modules/regulation-documents/dto/getRegulationDraftDocument.dto.ts deleted file mode 100644 index b7ff8545934b..000000000000 --- a/apps/download-service/src/app/modules/regulation-documents/dto/getRegulationDraftDocument.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsJWT } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class GetRegulationDraftDocumentDto { - @IsJWT() - @ApiProperty() - readonly __accessToken!: string -} 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 0887c6c656ef..c65e143c5a8e 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 @@ -1,17 +1,3 @@ -import { - Body, - Controller, - Header, - Post, - Res, - Param, - UseGuards, -} from '@nestjs/common' -import { ApiOkResponse } from '@nestjs/swagger' -import { Response } from 'express' -import { RegulationsAdminClientService } from '@island.is/clients/regulations-admin' -import { RegulationsService } from '@island.is/clients/regulations' -import { AdminPortalScope } from '@island.is/auth/scopes' import type { User } from '@island.is/auth-nest-tools' import { CurrentUser, @@ -19,11 +5,16 @@ import { Scopes, ScopesGuard, } from '@island.is/auth-nest-tools' -import { GetRegulationDraftDocumentDto } from './dto/getRegulationDraftDocument.dto' +import { AdminPortalScope } from '@island.is/auth/scopes' +import { RegulationsService } from '@island.is/clients/regulations' +import { RegulationsAdminClientService } from '@island.is/clients/regulations-admin' import { RegulationDraft, RegulationPdfInput, } from '@island.is/regulations/admin' +import { Controller, Header, Param, Post, Res, UseGuards } from '@nestjs/common' +import { ApiOkResponse } from '@nestjs/swagger' +import { Response } from 'express' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes( @@ -46,20 +37,15 @@ export class RegulationDocumentsController { async getDraftRegulationPdf( @Param('regulationId') regulationId: string, @CurrentUser() user: User, - @Body() resource: GetRegulationDraftDocumentDto, @Res() res: Response, ) { let draftRegulation: RegulationDraft | null = null - const authUser: User = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } try { draftRegulation = await this.regulationsAdminClientService.getDraftRegulation( regulationId, - authUser, + user, ) } catch (e) { console.error('unable to get draft regulation', e) @@ -92,7 +78,7 @@ export class RegulationDocumentsController { res.header('Content-Type', documentResponse.data.mimeType) res.header('Content-length', buffer.length.toString()) res.header('Content-Disposition', `inline; filename=${filename}`) - res.header('Cache-Control: no-cache') + res.header('Cache-Control', 'no-cache') return res.status(200).end(buffer) } diff --git a/apps/download-service/src/app/modules/vehicles-documents/dto/getVehicleHistoryDocument.dto.ts b/apps/download-service/src/app/modules/vehicles-documents/dto/getVehicleHistoryDocument.dto.ts deleted file mode 100644 index 64e5f4f0d489..000000000000 --- a/apps/download-service/src/app/modules/vehicles-documents/dto/getVehicleHistoryDocument.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsJWT, IsString, IsOptional } from 'class-validator' -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' - -export class GetVehicleHistoryDocumentDto { - @IsJWT() - @ApiProperty() - readonly __accessToken!: string - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly annualDoc: string | undefined -} diff --git a/apps/download-service/src/app/modules/vehicles-documents/vehicle-document.controller.ts b/apps/download-service/src/app/modules/vehicles-documents/vehicle-document.controller.ts index 76276c45133e..62367c608da2 100644 --- a/apps/download-service/src/app/modules/vehicles-documents/vehicle-document.controller.ts +++ b/apps/download-service/src/app/modules/vehicles-documents/vehicle-document.controller.ts @@ -1,17 +1,9 @@ -import { - Body, - Controller, - Header, - Post, - Res, - Param, - UseGuards, -} from '@nestjs/common' +import type { User } from '@island.is/auth-nest-tools' +import { AuthMiddleware } from '@island.is/auth-nest-tools' +import { ApiScope } from '@island.is/auth/scopes' +import { Controller, Header, Param, Post, Res, UseGuards } from '@nestjs/common' import { ApiOkResponse } from '@nestjs/swagger' import { Response } from 'express' -import { ApiScope } from '@island.is/auth/scopes' -import { AuthMiddleware } from '@island.is/auth-nest-tools' -import type { User } from '@island.is/auth-nest-tools' import { CurrentUser, @@ -19,9 +11,8 @@ import { Scopes, ScopesGuard, } from '@island.is/auth-nest-tools' -import { AuditService } from '@island.is/nest/audit' -import { GetVehicleHistoryDocumentDto } from './dto/getVehicleHistoryDocument.dto' import { PdfApi } from '@island.is/clients/vehicles' +import { AuditService } from '@island.is/nest/audit' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(ApiScope.vehicles) @@ -41,16 +32,10 @@ export class VehicleController { async getVehicleHistoryPdf( @Param('permno') permno: string, @CurrentUser() user: User, - @Body() resource: GetVehicleHistoryDocumentDto, @Res() res: Response, ) { - const authUser: User = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } - const documentResponse = await this.vehiclePDFService - .withMiddleware(new AuthMiddleware(authUser)) + .withMiddleware(new AuthMiddleware(user)) .vehicleReportPdfGet({ permno: permno }) if (documentResponse) { @@ -86,16 +71,10 @@ export class VehicleController { async getVehicleOwnership( @Param('ssn') ssn: string, @CurrentUser() user: User, - @Body() resource: GetVehicleHistoryDocumentDto, @Res() res: Response, ) { - const authUser: User = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } - const documentResponse = await this.vehiclePDFService - .withMiddleware(new AuthMiddleware(authUser)) + .withMiddleware(new AuthMiddleware(user)) .ownershipReportPdfGet({ ssn: ssn }) if (documentResponse) { @@ -113,10 +92,10 @@ export class VehicleController { 'Content-Disposition', `inline; filename=${user.nationalId}-eignaferill.pdf`, ) - res.header('Content-Type: application/pdf') - res.header('Pragma: no-cache') - res.header('Cache-Control: no-cache') - res.header('Cache-Control: nmax-age=0') + res.header('Content-Type', 'application/pdf') + res.header('Pragma', 'no-cache') + res.header('Cache-Control', 'no-cache') + res.header('Cache-Control', 'nmax-age=0') return res.status(200).end(buffer) } return res.end() diff --git a/apps/download-service/src/app/modules/work-machines-documents/dto/getWorkMachineCollectionDocument.dto.ts b/apps/download-service/src/app/modules/work-machines-documents/dto/getWorkMachineCollectionDocument.dto.ts deleted file mode 100644 index 2bac27910c59..000000000000 --- a/apps/download-service/src/app/modules/work-machines-documents/dto/getWorkMachineCollectionDocument.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsJWT } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class GetWorkMachineCollectionDocumentDto { - @IsJWT() - @ApiProperty() - readonly __accessToken!: string -} diff --git a/apps/download-service/src/app/modules/work-machines-documents/work-machines-documents.controller.ts b/apps/download-service/src/app/modules/work-machines-documents/work-machines-documents.controller.ts index e5112f83a75d..a9a27ac310a7 100644 --- a/apps/download-service/src/app/modules/work-machines-documents/work-machines-documents.controller.ts +++ b/apps/download-service/src/app/modules/work-machines-documents/work-machines-documents.controller.ts @@ -1,17 +1,8 @@ -import { - Body, - Controller, - Header, - Post, - Res, - Param, - UseGuards, -} from '@nestjs/common' +import type { User } from '@island.is/auth-nest-tools' +import { ApiScope } from '@island.is/auth/scopes' +import { Controller, Header, Param, Post, Res, UseGuards } from '@nestjs/common' import { ApiOkResponse } from '@nestjs/swagger' import { Response } from 'express' -import { ApiScope } from '@island.is/auth/scopes' -import { AuthMiddleware } from '@island.is/auth-nest-tools' -import type { User } from '@island.is/auth-nest-tools' import { CurrentUser, @@ -19,9 +10,8 @@ import { Scopes, ScopesGuard, } from '@island.is/auth-nest-tools' -import { AuditService } from '@island.is/nest/audit' -import { GetWorkMachineCollectionDocumentDto } from './dto/getWorkMachineCollectionDocument.dto' import { WorkMachinesClientService } from '@island.is/clients/work-machines' +import { AuditService } from '@island.is/nest/audit' @UseGuards(IdsUserGuard, ScopesGuard) @Scopes(ApiScope.vehicles) @@ -41,15 +31,9 @@ export class WorkMachinesController { async getWorkMachinesCollection( @Param('fileType') fileType: 'csv' | 'excel', @CurrentUser() user: User, - @Body() resource: GetWorkMachineCollectionDocumentDto, @Res() res: Response, ) { - const authUser: User = { - ...user, - authorization: `Bearer ${resource.__accessToken}`, - } - - const documentResponse = await this.docService.getDocuments(authUser, { + const documentResponse = await this.docService.getDocuments(user, { fileType, }) @@ -71,9 +55,9 @@ export class WorkMachinesController { }`, ) res.header('Content-Type: application/octet-stream') - res.header('Pragma: no-cache') - res.header('Cache-Control: no-cache') - res.header('Cache-Control: nmax-age=0') + res.header('Pragma', 'no-cache') + res.header('Cache-Control', 'no-cache') + res.header('Cache-Control', 'nmax-age=0') return res.status(200).end(buffer) } return res.end() diff --git a/apps/financial-aid/api/scripts/run-xroad-proxy.sh b/apps/financial-aid/api/scripts/run-xroad-proxy.sh index edb2e6ef14f5..ba1fd41f25f8 100755 --- a/apps/financial-aid/api/scripts/run-xroad-proxy.sh +++ b/apps/financial-aid/api/scripts/run-xroad-proxy.sh @@ -1,17 +1,17 @@ #!/bin/bash INSTANCE_ID=$( - aws ec2 describe-instances \ - --filters "Name=tag:Name,Values=Bastion Host" "Name=instance-state-name,Values=running" \ - --query "Reservations[0].Instances[0].InstanceId" \ - --region eu-west-1 \ - --output text + aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=Bastion Host" "Name=instance-state-name,Values=running" \ + --query "Reservations[0].Instances[0].InstanceId" \ + --region eu-west-1 \ + --output text ) echo "Starting port forwarding session with instance $INSTANCE_ID for profile $AWS_PROFILE" aws ssm start-session \ - --target "$INSTANCE_ID" \ - --document-name AWS-StartPortForwardingSession \ - --parameters '{"portNumber":["8081"],"localPortNumber":["5050"]}' \ - --region eu-west-1 + --target "$INSTANCE_ID" \ + --document-name AWS-StartPortForwardingSession \ + --parameters '{"portNumber":["8081"],"localPortNumber":["5050"]}' \ + --region eu-west-1 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 5a42b29ab8a7..099c3bf98b10 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 @@ -41,6 +41,7 @@ import { Institution } from '../institution' import { PoliceCaseFile, PoliceCaseInfo, + SubpoenaStatus, UploadPoliceCaseFileResponse, } from '../police' import { backendModuleConfig } from './backend.config' @@ -306,6 +307,13 @@ export class BackendService extends DataSource<{ req: Request }> { return this.get(`case/${caseId}/policeFiles`) } + getSubpoenaStatus( + caseId: string, + subpoenaId: string, + ): Promise { + return this.get(`case/${caseId}/subpoenaStatus/${subpoenaId}`) + } + getPoliceCaseInfo(caseId: string): Promise { return this.get(`case/${caseId}/policeCaseInfo`) } diff --git a/apps/judicial-system/api/src/app/modules/defendant/index.ts b/apps/judicial-system/api/src/app/modules/defendant/index.ts index 0811956a0ca8..040ddad841e3 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/index.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/index.ts @@ -2,3 +2,4 @@ export { Defendant } from './models/defendant.model' export { DeleteDefendantResponse } from './models/delete.response' export { CivilClaimant } from './models/civilClaimant.model' export { DeleteCivilClaimantResponse } from './models/deleteCivilClaimant.response' +export { Subpoena } from './models/subpoena.model' diff --git a/apps/judicial-system/api/src/app/modules/defendant/models/subpoena.model.ts b/apps/judicial-system/api/src/app/modules/defendant/models/subpoena.model.ts index 9a810b1edc10..2bd991c2d4d8 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/models/subpoena.model.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/models/subpoena.model.ts @@ -1,4 +1,8 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql' +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql' + +import { ServiceStatus } from '@island.is/judicial-system/types' + +registerEnumType(ServiceStatus, { name: 'ServiceStatus' }) @ObjectType() export class Subpoena { @@ -14,15 +18,27 @@ export class Subpoena { @Field(() => String, { nullable: true }) subpoenaId?: string - @Field(() => Boolean, { nullable: true }) - acknowledged?: boolean + @Field(() => String, { nullable: true }) + defendantId?: string + + @Field(() => String, { nullable: true }) + caseId?: string + + @Field(() => ServiceStatus, { nullable: true }) + serviceStatus?: ServiceStatus + + @Field(() => String, { nullable: true }) + serviceDate?: string @Field(() => String, { nullable: true }) - registeredBy?: string + servedBy?: string @Field(() => String, { nullable: true }) comment?: string + @Field(() => String, { nullable: true }) + defenderNationalId?: string + @Field(() => String, { nullable: true }) arraignmentDate?: 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 d278438871f3..ecf113cd4174 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 @@ -182,16 +182,18 @@ export class FileController { getSubpoenaPdf( @Param('id') id: string, @Param('defendantId') defendantId: string, - @Param('subpoenaId') subpoenaId: string, @CurrentHttpUser() user: User, @Req() req: Request, @Res() res: Response, + @Param('subpoenaId') subpoenaId?: string, @Query('arraignmentDate') arraignmentDate?: string, @Query('location') location?: string, @Query('subpoenaType') subpoenaType?: SubpoenaType, ): Promise { this.logger.debug( - `Getting the subpoena for defendant ${defendantId} of case ${id} as a pdf document`, + `Getting subpoena ${ + subpoenaId ?? 'draft' + } for defendant ${defendantId} of case ${id} as a pdf document`, ) const subpoenaIdInjection = subpoenaId ? `/${subpoenaId}` : '' 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 247f47218ef4..cfc4f9447db8 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 @@ -176,27 +176,25 @@ export class LimitedAccessFileController { ) } - @Get('subpoena/:defendantId') + @Get('subpoena/:defendantId/:subpoenaId') @Header('Content-Type', 'application/pdf') getSubpoenaPdf( @Param('id') id: string, @Param('defendantId') defendantId: string, - @Query('arraignmentDate') arraignmentDate: string, - @Query('location') location: string, - @Query('subpoenaType') subpoenaType: SubpoenaType, + @Param('subpoenaId') subpoenaId: string, @CurrentHttpUser() user: User, @Req() req: Request, @Res() res: Response, ): Promise { this.logger.debug( - `Getting the subpoena for defendant ${defendantId} of case ${id} as a pdf document`, + `Getting subpoena ${subpoenaId} for defendant ${defendantId} of case ${id} as a pdf document`, ) return this.fileService.tryGetFile( user.id, AuditedAction.GET_SUBPOENA_PDF, id, - `limitedAccess/defendant/${defendantId}/subpoena?arraignmentDate=${arraignmentDate}&location=${location}&subpoenaType=${subpoenaType}`, + `limitedAccess/defendant/${defendantId}/subpoena/${subpoenaId}`, req, res, 'pdf', diff --git a/apps/judicial-system/api/src/app/modules/police/dto/subpoenaStatus.input.ts b/apps/judicial-system/api/src/app/modules/police/dto/subpoenaStatus.input.ts new file mode 100644 index 000000000000..dcc11761c0be --- /dev/null +++ b/apps/judicial-system/api/src/app/modules/police/dto/subpoenaStatus.input.ts @@ -0,0 +1,14 @@ +import { Allow } from 'class-validator' + +import { Field, ID, InputType } from '@nestjs/graphql' + +@InputType() +export class SubpoenaStatusQueryInput { + @Allow() + @Field(() => ID) + readonly caseId!: string + + @Allow() + @Field(() => ID) + readonly subpoenaId!: string +} diff --git a/apps/judicial-system/api/src/app/modules/police/index.ts b/apps/judicial-system/api/src/app/modules/police/index.ts index c2c2457a57b5..a1fe72f38c8e 100644 --- a/apps/judicial-system/api/src/app/modules/police/index.ts +++ b/apps/judicial-system/api/src/app/modules/police/index.ts @@ -1,3 +1,4 @@ export { PoliceCaseInfo } from './models/policeCaseInfo.model' +export { SubpoenaStatus } from './models/subpoenaStatus.model' export { PoliceCaseFile } from './models/policeCaseFile.model' export { UploadPoliceCaseFileResponse } from './models/uploadPoliceCaseFile.response' diff --git a/apps/judicial-system/api/src/app/modules/police/models/subpoenaStatus.model.ts b/apps/judicial-system/api/src/app/modules/police/models/subpoenaStatus.model.ts new file mode 100644 index 000000000000..b4617bfe58a4 --- /dev/null +++ b/apps/judicial-system/api/src/app/modules/police/models/subpoenaStatus.model.ts @@ -0,0 +1,21 @@ +import { Field, ObjectType } from '@nestjs/graphql' + +import { ServiceStatus } from '@island.is/judicial-system/types' + +@ObjectType() +export class SubpoenaStatus { + @Field(() => ServiceStatus) + readonly serviceStatus!: ServiceStatus + + @Field(() => String, { nullable: true }) + readonly servedBy?: string + + @Field(() => String, { nullable: true }) + readonly comment?: string + + @Field(() => String, { nullable: true }) + readonly serviceDate?: string + + @Field(() => String, { nullable: true }) + readonly defenderNationalId?: string +} diff --git a/apps/judicial-system/api/src/app/modules/police/police.resolver.ts b/apps/judicial-system/api/src/app/modules/police/police.resolver.ts index 8d4fbf9cd90a..1eef19336e76 100644 --- a/apps/judicial-system/api/src/app/modules/police/police.resolver.ts +++ b/apps/judicial-system/api/src/app/modules/police/police.resolver.ts @@ -17,9 +17,11 @@ import type { User } from '@island.is/judicial-system/types' import { BackendService } from '../backend' import { PoliceCaseFilesQueryInput } from './dto/policeCaseFiles.input' import { PoliceCaseInfoQueryInput } from './dto/policeCaseInfo.input' +import { SubpoenaStatusQueryInput } from './dto/subpoenaStatus.input' import { UploadPoliceCaseFileInput } from './dto/uploadPoliceCaseFile.input' import { PoliceCaseFile } from './models/policeCaseFile.model' import { PoliceCaseInfo } from './models/policeCaseInfo.model' +import { SubpoenaStatus } from './models/subpoenaStatus.model' import { UploadPoliceCaseFileResponse } from './models/uploadPoliceCaseFile.response' @UseGuards(JwtGraphQlAuthGuard) @@ -49,6 +51,26 @@ export class PoliceResolver { ) } + @Query(() => SubpoenaStatus, { nullable: true }) + subpoenaStatus( + @Args('input', { type: () => SubpoenaStatusQueryInput }) + input: SubpoenaStatusQueryInput, + @CurrentGraphQlUser() user: User, + @Context('dataSources') + { backendService }: { backendService: BackendService }, + ): Promise { + this.logger.debug( + `Getting subpoena status for subpoena ${input.subpoenaId} of case ${input.caseId}`, + ) + + return this.auditTrailService.audit( + user.id, + AuditedAction.GET_SUBPOENA_STATUS, + backendService.getSubpoenaStatus(input.caseId, input.subpoenaId), + input.caseId, + ) + } + @Query(() => [PoliceCaseInfo], { nullable: true }) policeCaseInfo( @Args('input', { type: () => PoliceCaseInfoQueryInput }) diff --git a/apps/judicial-system/backend/migrations/20240925130059-update-subpoena.js b/apps/judicial-system/backend/migrations/20240925130059-update-subpoena.js new file mode 100644 index 000000000000..43ae74f21010 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240925130059-update-subpoena.js @@ -0,0 +1,87 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((transaction) => + queryInterface + .changeColumn( + 'subpoena', + 'acknowledged', + { type: Sequelize.STRING, allowNull: true }, + { transaction }, + ) + .then( + () => + queryInterface.renameColumn( + 'subpoena', + 'acknowledged', + 'service_status', + { transaction }, + ), + + queryInterface.renameColumn( + 'subpoena', + 'acknowledged_date', + 'service_date', + { transaction }, + ), + + queryInterface.renameColumn( + 'subpoena', + 'registered_by', + 'served_by', + { transaction }, + ), + + queryInterface.addColumn( + 'subpoena', + 'defender_national_id', + { + type: Sequelize.STRING, + allowNull: true, + }, + { transaction }, + ), + ), + ) + }, + + down: (queryInterface) => { + return queryInterface.sequelize.transaction((transaction) => + queryInterface + .renameColumn('subpoena', 'service_status', 'acknowledged', { + transaction, + }) + .then( + () => + queryInterface.changeColumn( + 'subpoena', + 'acknowledged', + { + type: 'BOOLEAN USING CAST("acknowledged" as BOOLEAN)', + allowNull: true, + }, + { transaction }, + ), + + queryInterface.renameColumn( + 'subpoena', + 'service_date', + 'acknowledged_date', + { transaction }, + ), + + queryInterface.renameColumn( + 'subpoena', + 'served_by', + 'registered_by', + { transaction }, + ), + + queryInterface.removeColumn('subpoena', 'defender_national_id', { + transaction, + }), + ), + ) + }, +} diff --git a/apps/judicial-system/backend/migrations/20241002084126-update-subpoena.js b/apps/judicial-system/backend/migrations/20241002084126-update-subpoena.js new file mode 100644 index 000000000000..4903ef1d68fe --- /dev/null +++ b/apps/judicial-system/backend/migrations/20241002084126-update-subpoena.js @@ -0,0 +1,22 @@ +'use strict' + +module.exports = { + up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.addColumn( + 'subpoena', + 'hash', + { type: Sequelize.STRING, allowNull: true }, + { transaction }, + ), + ) + }, + + down(queryInterface) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.removeColumn('subpoena', 'hash', { + transaction, + }), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts index 95567e0bd7a4..8d2e980a2c9a 100644 --- a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts @@ -22,6 +22,7 @@ import { addMediumText, addNormalRightAlignedText, addNormalText, + Confirmation, setTitle, } from './pdfHelpers' @@ -33,6 +34,7 @@ export const createSubpoena = ( arraignmentDate?: Date, location?: string, subpoenaType?: SubpoenaType, + confirmation?: Confirmation, ): Promise => { const doc = new PDFDocument({ size: 'A4', @@ -51,7 +53,7 @@ export const createSubpoena = ( setTitle(doc, formatMessage(strings.title)) - if (subpoena) { + if (confirmation) { addEmptyLines(doc, 5) } @@ -154,13 +156,8 @@ export const createSubpoena = ( addFooter(doc) - if (subpoena) { - addConfirmation(doc, { - actor: theCase.judge?.name || '', - title: theCase.judge?.title, - institution: theCase.judge?.institution?.name || '', - date: subpoena.created, - }) + if (confirmation) { + addConfirmation(doc, confirmation) } doc.end() 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 26377502db54..dd1271f2ba0b 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 @@ -1,7 +1,6 @@ import CryptoJS from 'crypto-js' import format from 'date-fns/format' import { Base64 } from 'js-base64' -import { Op } from 'sequelize' import { Sequelize } from 'sequelize-typescript' import { diff --git a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts index 10065ea3bdb6..860356035526 100644 --- a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts @@ -2,6 +2,7 @@ import CryptoJS from 'crypto-js' import { BadRequestException, + forwardRef, Inject, Injectable, InternalServerErrorException, @@ -33,7 +34,7 @@ import { } from '../../formatters' import { AwsS3Service } from '../aws-s3' import { Defendant } from '../defendant' -import { Subpoena } from '../subpoena' +import { Subpoena, SubpoenaService } from '../subpoena' import { UserService } from '../user' import { Case } from './models/case.model' @@ -45,6 +46,8 @@ export class PdfService { private readonly awsS3Service: AwsS3Service, private readonly intlService: IntlService, private readonly userService: UserService, + @Inject(forwardRef(() => SubpoenaService)) + private readonly subpoenaService: SubpoenaService, @InjectModel(Case) private readonly caseModel: typeof Case, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -298,9 +301,31 @@ export class PdfService { location?: string, subpoenaType?: SubpoenaType, ): Promise { + let confirmation: Confirmation | undefined = undefined + + if (subpoena) { + if (subpoena.hash) { + const existingPdf = await this.tryGetPdfFromS3( + theCase, + `${theCase.id}/subpoena/${subpoena.id}.pdf`, + ) + + if (existingPdf) { + return existingPdf + } + } + + confirmation = { + actor: theCase.judge?.name ?? '', + title: theCase.judge?.title, + institution: theCase.judge?.institution?.name ?? '', + date: subpoena.created, + } + } + await this.refreshFormatMessage() - return createSubpoena( + const generatedPdf = await createSubpoena( theCase, defendant, this.formatMessage, @@ -308,6 +333,26 @@ export class PdfService { arraignmentDate, location, subpoenaType, + confirmation, ) + + if (subpoena) { + const subpoenaHash = CryptoJS.MD5( + generatedPdf.toString('binary'), + ).toString(CryptoJS.enc.Hex) + + // No need to wait for this to finish + this.subpoenaService + .setHash(subpoena.id, subpoenaHash) + .then(() => + this.tryUploadPdfToS3( + theCase, + `${theCase.id}/subpoena/${subpoena.id}.pdf`, + generatedPdf, + ), + ) + } + + return generatedPdf } } 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 d86a9827ce80..b84a86bd3d9d 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 @@ -1,5 +1,3 @@ -import { CanActivate } from '@nestjs/common' - import { JwtAuthGuard, RolesGuard } from '@island.is/judicial-system/auth' import { indictmentCases } from '@island.is/judicial-system/types' diff --git a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.controller.ts b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.controller.ts index 58cff3c4941b..439904735d4e 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.controller.ts @@ -19,7 +19,13 @@ import { RolesRules, } from '@island.is/judicial-system/auth' -import { prosecutorRepresentativeRule, prosecutorRule } from '../../guards' +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../guards' import { Case, CaseExistsGuard, CaseWriteGuard, CurrentCase } from '../case' import { UpdateCivilClaimantDto } from './dto/updateCivilClaimant.dto' import { CivilClaimant } from './models/civilClaimant.model' @@ -36,7 +42,13 @@ export class CivilClaimantController { ) {} @UseGuards(CaseExistsGuard, CaseWriteGuard) - @RolesRules(prosecutorRule, prosecutorRepresentativeRule) + @RolesRules( + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ) @Post() @ApiCreatedResponse({ type: CivilClaimant, @@ -52,7 +64,13 @@ export class CivilClaimantController { } @UseGuards(CaseExistsGuard, CaseWriteGuard) - @RolesRules(prosecutorRule, prosecutorRepresentativeRule) + @RolesRules( + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ) @Patch(':civilClaimantId') @ApiOkResponse({ type: CivilClaimant, 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 aeac0ff7f26d..56c762f6299a 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,7 +18,6 @@ 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 { eventModuleConfig } from './event.config' 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 88c3d0946486..e23a72d4273b 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 @@ -1,5 +1,3 @@ -import { CanActivate } from '@nestjs/common' - import { RolesGuard } from '@island.is/judicial-system/auth' import { CaseExistsGuard, CaseReadGuard } from '../../../case' diff --git a/apps/judicial-system/backend/src/app/modules/police/police.controller.ts b/apps/judicial-system/backend/src/app/modules/police/police.controller.ts index 2d6321e6bc14..7eaa7773c91b 100644 --- a/apps/judicial-system/backend/src/app/modules/police/police.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/police/police.controller.ts @@ -21,7 +21,11 @@ import { } from '@island.is/judicial-system/auth' import type { User } from '@island.is/judicial-system/types' -import { prosecutorRepresentativeRule, prosecutorRule } from '../../guards' +import { + districtCourtJudgeRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../guards' import { Case, CaseExistsGuard, @@ -30,6 +34,7 @@ import { CaseReadGuard, CurrentCase, } from '../case' +import { Subpoena } from '../subpoena' import { UploadPoliceCaseFileDto } from './dto/uploadPoliceCaseFile.dto' import { PoliceCaseFile } from './models/policeCaseFile.model' import { PoliceCaseInfo } from './models/policeCaseInfo.model' @@ -69,6 +74,26 @@ export class PoliceController { return this.policeService.getAllPoliceCaseFiles(theCase.id, user) } + @RolesRules( + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + ) + @Get('subpoenaStatus/:subpoenaId') + @ApiOkResponse({ + type: Subpoena, + description: 'Gets subpoena status', + }) + getSubpoenaStatus( + @Param('subpoenaId') subpoenaId: string, + @CurrentCase() theCase: Case, + @CurrentHttpUser() user: User, + ): Promise { + this.logger.debug(`Gets subpoena status in case ${theCase.id}`) + + return this.policeService.getSubpoenaStatus(subpoenaId, user) + } + @RolesRules(prosecutorRule, prosecutorRepresentativeRule) @UseInterceptors(CaseOriginalAncestorInterceptor) @Get('policeCaseInfo') diff --git a/apps/judicial-system/backend/src/app/modules/police/police.module.ts b/apps/judicial-system/backend/src/app/modules/police/police.module.ts index 37f05ff5e4dc..1df72c2b8794 100644 --- a/apps/judicial-system/backend/src/app/modules/police/police.module.ts +++ b/apps/judicial-system/backend/src/app/modules/police/police.module.ts @@ -1,6 +1,6 @@ import { forwardRef, Module } from '@nestjs/common' -import { AwsS3Module, CaseModule, EventModule } from '../index' +import { AwsS3Module, CaseModule, EventModule, SubpoenaModule } from '../index' import { PoliceController } from './police.controller' import { PoliceService } from './police.service' @@ -9,6 +9,7 @@ import { PoliceService } from './police.service' forwardRef(() => CaseModule), forwardRef(() => EventModule), forwardRef(() => AwsS3Module), + forwardRef(() => SubpoenaModule), ], providers: [PoliceService], exports: [PoliceService], 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 c4659a31c6de..b4a9d5e2848a 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 @@ -23,13 +23,19 @@ import { import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import type { User } from '@island.is/judicial-system/types' -import { CaseState, CaseType } from '@island.is/judicial-system/types' +import { + CaseState, + CaseType, + ServiceStatus, +} 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 { Subpoena, SubpoenaService } from '../subpoena' +import { UpdateSubpoenaDto } from '../subpoena/dto/updateSubpoena.dto' import { UploadPoliceCaseFileDto } from './dto/uploadPoliceCaseFile.dto' import { CreateSubpoenaResponse } from './models/createSubpoena.response' import { PoliceCaseFile } from './models/policeCaseFile.model' @@ -122,6 +128,18 @@ export class PoliceService { skjol: z.optional(z.array(this.policeCaseFileStructure)), malseinings: z.optional(z.array(this.crimeSceneStructure)), }) + private subpoenaStructure = z.object({ + acknowledged: z.boolean().nullish(), + comment: z.string().nullish(), + defenderChoice: z.string().nullish(), + defenderNationalId: z.string().nullish(), + prosecutedConfirmedSubpoenaThroughIslandis: z.boolean().nullish(), + servedBy: z.string().nullish(), + servedAt: z.string().nullish(), + delivered: z.boolean().nullish(), + deliveredOnPaper: z.boolean().nullish(), + deliveredToLawyer: z.boolean().nullish(), + }) constructor( @Inject(policeModuleConfig.KEY) @@ -130,6 +148,8 @@ export class PoliceService { private readonly eventService: EventService, @Inject(forwardRef(() => AwsS3Service)) private readonly awsS3Service: AwsS3Service, + @Inject(forwardRef(() => SubpoenaService)) + private readonly subpoenaService: SubpoenaService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) { this.xRoadPath = createXRoadAPIPath( @@ -316,6 +336,85 @@ export class PoliceService { }) } + async getSubpoenaStatus(subpoenaId: string, user: User): Promise { + return this.fetchPoliceDocumentApi( + `${this.xRoadPath}/GetSubpoenaStatus?id=${subpoenaId}`, + ) + .then(async (res: Response) => { + if (res.ok) { + const response: z.infer = + await res.json() + + this.subpoenaStructure.parse(response) + + const subpoenaToUpdate = await this.subpoenaService.findBySubpoenaId( + subpoenaId, + ) + + const updatedSubpoena = await this.subpoenaService.update( + subpoenaToUpdate, + { + comment: response.comment ?? undefined, + servedBy: response.servedBy ?? undefined, + defenderNationalId: response.defenderNationalId ?? undefined, + serviceDate: response.servedAt ?? undefined, + serviceStatus: response.deliveredToLawyer + ? ServiceStatus.DEFENDER + : response.prosecutedConfirmedSubpoenaThroughIslandis + ? ServiceStatus.ELECTRONICALLY + : response.deliveredOnPaper || response.delivered === true + ? ServiceStatus.IN_PERSON + : response.acknowledged === false + ? ServiceStatus.FAILED + : // TODO: handle expired + undefined, + } as UpdateSubpoenaDto, + ) + + return updatedSubpoena + } + + const reason = await res.text() + + // The police system does not provide a structured error response. + // When a subpoena does not exist, a stack trace is returned. + throw new NotFoundException({ + message: `Subpoena with id ${subpoenaId} does not exist`, + detail: reason, + }) + }) + .catch((reason) => { + if (reason instanceof NotFoundException) { + throw reason + } + + if (reason instanceof ServiceUnavailableException) { + // Act as if the case does not exist + throw new NotFoundException({ + ...reason, + message: `Subpoena ${subpoenaId} does not exist`, + detail: reason.message, + }) + } + + this.eventService.postErrorEvent( + 'Failed to get subpoena', + { + subpoenaId, + actor: user.name, + institution: user.institution?.name, + }, + reason, + ) + + throw new BadGatewayException({ + ...reason, + message: `Failed to get subpoena ${subpoenaId}`, + detail: reason.message, + }) + }) + } + async getPoliceCaseInfo( caseId: string, user: User, @@ -514,6 +613,7 @@ export class PoliceService { workingCase: Case, defendant: Defendant, subpoena: string, + indictment: string, user: User, ): Promise { const { courtCaseNumber, dateLogs, prosecutor, policeCaseNumbers, court } = @@ -542,7 +642,7 @@ export class PoliceService { agent: this.agent, body: JSON.stringify({ documentName: documentName, - documentBase64: subpoena, + documentsBase64: [subpoena, indictment], courtRegistrationDate: arraignmentInfo?.date, prosecutorSsn: prosecutor?.nationalId, prosecutedSsn: normalizedNationalId, @@ -552,16 +652,17 @@ export class PoliceService { lokeCaseNumber: policeCaseNumbers?.[0], courtCaseNumber: courtCaseNumber, fileTypeCode: 'BRTNG', + rvgCaseId: workingCase.id, }), } as RequestInit, ) - if (!res.ok) { - throw await res.json() + if (res.ok) { + const subpoenaResponse = await res.json() + return { subpoenaId: subpoenaResponse.id } } - const subpoenaId = await res.json() - return { subpoenaId } + throw await res.text() } catch (error) { this.logger.error(`Failed create subpoena for case ${workingCase.id}`, { error, diff --git a/apps/judicial-system/backend/src/app/modules/police/test/createTestingPoliceModule.ts b/apps/judicial-system/backend/src/app/modules/police/test/createTestingPoliceModule.ts index fb00a32daf9b..c97b49711f23 100644 --- a/apps/judicial-system/backend/src/app/modules/police/test/createTestingPoliceModule.ts +++ b/apps/judicial-system/backend/src/app/modules/police/test/createTestingPoliceModule.ts @@ -12,6 +12,7 @@ import { AwsS3Service } from '../../aws-s3' import { CaseService } from '../../case' import { InternalCaseService } from '../../case/internalCase.service' import { EventService } from '../../event' +import { SubpoenaService } from '../../subpoena' import { policeModuleConfig } from '../police.config' import { PoliceController } from '../police.controller' import { PoliceService } from '../police.service' @@ -20,6 +21,7 @@ jest.mock('../../event/event.service') jest.mock('../../aws-s3/awsS3.service.ts') jest.mock('../../case/case.service.ts') jest.mock('../../case/internalCase.service.ts') +jest.mock('../../subpoena/subpoena.service.ts') export const createTestingPoliceModule = async () => { const policeModule = await Test.createTestingModule({ @@ -36,6 +38,7 @@ export const createTestingPoliceModule = async () => { CaseService, InternalCaseService, PoliceService, + SubpoenaService, { provide: LOGGER_PROVIDER, useValue: { 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 index b482b972be30..1b93bdd29b27 100644 --- 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 @@ -1,19 +1,24 @@ -import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' +import { IsDate, IsEnum, IsOptional, IsString } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' -import { DefenderChoice } from '@island.is/judicial-system/types' +import { DefenderChoice, ServiceStatus } from '@island.is/judicial-system/types' export class UpdateSubpoenaDto { @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean }) - readonly acknowledged?: boolean + @IsEnum(ServiceStatus) + @ApiPropertyOptional({ enum: ServiceStatus }) + readonly serviceStatus?: ServiceStatus @IsOptional() @IsString() @ApiPropertyOptional({ type: String }) - readonly registeredBy?: string + readonly servedBy?: string + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly serviceDate?: string @IsOptional() @IsString() 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 index 624bd2d361c1..f5dc6c406cb1 100644 --- 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 @@ -24,11 +24,13 @@ export class SubpoenaExistsGuard implements CanActivate { const defendant: Defendant = request.defendant if (!defendant) { + // subpoenaId is the external police document id request.subpoena = await this.subpoenaService.findBySubpoenaId(subpoenaId) return true } + // subpoenaId is the internal subpoena id const subpoena = defendant.subpoenas?.find( (subpoena) => subpoena.id === subpoenaId, ) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts index 9a4c195e465e..3db547d3c02c 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts @@ -20,7 +20,7 @@ import { RolesGuard, RolesRules, } from '@island.is/judicial-system/auth' -import { indictmentCases, SubpoenaType } from '@island.is/judicial-system/types' +import { indictmentCases } from '@island.is/judicial-system/types' import { defenderRule } from '../../guards' import { @@ -37,7 +37,6 @@ import { SubpoenaExistsOptionalGuard } from './guards/subpoenaExists.guard' import { Subpoena } from './models/subpoena.model' @Controller([ - 'api/case/:caseId/limitedAccess/defendant/:defendantId/subpoena', 'api/case/:caseId/limitedAccess/defendant/:defendantId/subpoena/:subpoenaId', ]) @UseGuards( @@ -71,23 +70,15 @@ export class LimitedAccessSubpoenaController { @CurrentDefendant() defendant: Defendant, @CurrentSubpoena() subpoena: Subpoena, @Res() res: Response, - @Query('arraignmentDate') arraignmentDate?: Date, - @Query('location') location?: string, - @Query('subpoenaType') subpoenaType?: SubpoenaType, ): Promise { this.logger.debug( - `Getting subpoena ${ - subpoenaId ?? 'draft' - } for defendant ${defendantId} of case ${caseId} as a pdf document`, + `Getting subpoena ${subpoenaId} for defendant ${defendantId} of case ${caseId} as a pdf document`, ) const pdf = await this.pdfService.getSubpoenaPdf( theCase, defendant, subpoena, - arraignmentDate, - location, - subpoenaType, ) res.end(pdf) 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 index 4d4f05a53b67..513d5daa5a2d 100644 --- 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 @@ -11,6 +11,8 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { ServiceStatus } from '@island.is/judicial-system/types' + import { Case } from '../../case/models/case.model' import { Defendant } from '../../defendant/models/defendant.model' @@ -57,18 +59,30 @@ export class Subpoena extends Model { @ApiPropertyOptional({ type: Case }) case?: Case - @Column({ type: DataType.BOOLEAN, allowNull: true }) - @ApiPropertyOptional({ type: Boolean }) - acknowledged?: boolean + @Column({ + type: DataType.ENUM, + allowNull: true, + values: Object.values(ServiceStatus), + }) + @ApiPropertyOptional({ enum: ServiceStatus }) + serviceStatus?: ServiceStatus + + @Column({ type: DataType.DATE, allowNull: true }) + @ApiPropertyOptional({ type: Date }) + serviceDate?: Date @Column({ type: DataType.STRING, allowNull: true }) @ApiPropertyOptional({ type: String }) - registeredBy?: string + servedBy?: string @Column({ type: DataType.TEXT, allowNull: true }) @ApiPropertyOptional({ type: String }) comment?: string + @Column({ type: DataType.STRING, allowNull: true }) + @ApiPropertyOptional({ type: String }) + defenderNationalId?: string + @Column({ type: DataType.DATE, allowNull: false }) @ApiProperty({ type: Date }) arraignmentDate!: Date @@ -76,4 +90,8 @@ export class Subpoena extends Model { @Column({ type: DataType.STRING, allowNull: false }) @ApiProperty({ type: String }) location!: string + + @Column({ type: DataType.STRING, allowNull: true }) + @ApiPropertyOptional({ type: String }) + hash?: string } 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 aa2fe114ef7f..179b3ae41d3a 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 @@ -2,7 +2,12 @@ import { Base64 } from 'js-base64' import { Includeable, Sequelize } from 'sequelize' import { Transaction } from 'sequelize/types' -import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { + forwardRef, + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common' import { InjectConnection, InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' @@ -52,6 +57,22 @@ export class SubpoenaService { ) } + async setHash(id: string, hash: string): Promise { + const [numberOfAffectedRows] = await this.subpoenaModel.update( + { hash }, + { where: { id } }, + ) + + if (numberOfAffectedRows > 1) { + // Tolerate failure, but log error + this.logger.error( + `Unexpected number of rows (${numberOfAffectedRows}) affected when updating subpoena hash for subpoena ${id}`, + ) + } else if (numberOfAffectedRows < 1) { + throw new InternalServerErrorException(`Could not update subpoena ${id}`) + } + } + async update( subpoena: Subpoena, update: UpdateSubpoenaDto, @@ -99,6 +120,7 @@ export class SubpoenaService { } const updatedSubpoena = await this.findBySubpoenaId(subpoena.subpoenaId) + return updatedSubpoena } @@ -113,7 +135,7 @@ export class SubpoenaService { }) if (!subpoena) { - throw new Error(`Subpoena with id ${subpoenaId} not found`) + throw new Error(`Subpoena with subpoena id ${subpoenaId} not found`) } return subpoena @@ -126,16 +148,19 @@ export class SubpoenaService { user: User, ): Promise { try { - const pdf = await this.pdfService.getSubpoenaPdf( + const subpoenaPdf = await this.pdfService.getSubpoenaPdf( theCase, defendant, subpoena, ) + const indictmentPdf = await this.pdfService.getIndictmentPdf(theCase) + const createdSubpoena = await this.policeService.createSubpoena( theCase, defendant, - Base64.btoa(pdf.toString('binary')), + Base64.btoa(subpoenaPdf.toString('binary')), + Base64.btoa(indictmentPdf.toString('binary')), user, ) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdf.spec.ts index cda75ddd2fbe..880f13e6778c 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdf.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdf.spec.ts @@ -63,9 +63,6 @@ describe('LimitedAccessSubpoenaController - Get subpoena pdf', () => { theCase, defendant, subpoena, - undefined, - undefined, - undefined, ) expect(res.end).toHaveBeenCalledWith(pdf) }) diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts index 7889f054a2e9..6398f668c5e7 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts @@ -1,7 +1,10 @@ import { ApiProperty } from '@nestjs/swagger' import { formatDate } from '@island.is/judicial-system/formatters' -import { DateType } from '@island.is/judicial-system/types' +import { + DateType, + isSuccessfulServiceStatus, +} from '@island.is/judicial-system/types' import { InternalCaseResponse } from './internal/internalCase.response' import { Groups } from './shared/groups.model' @@ -41,7 +44,10 @@ export class CaseResponse { caseId: internalCase.id, data: { caseNumber: `${t.caseNumber} ${internalCase.courtCaseNumber}`, - hasBeenServed: subpoenas.length > 0 ? subpoenas[0].acknowledged : false, + hasBeenServed: + subpoenas.length > 0 + ? isSuccessfulServiceStatus(subpoenas[0].serviceStatus) + : false, groups: [ { label: t.defendant, diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts index 72bd4ccfa847..efff1d029dd3 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts @@ -3,6 +3,7 @@ import { DefenderChoice, Gender, Institution, + ServiceStatus, User, } from '@island.is/judicial-system/types' @@ -42,5 +43,5 @@ interface DateLog { interface Subpoena { id: string subpoenaId: string - acknowledged: boolean + serviceStatus?: ServiceStatus } diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts index 342e0ed294f5..0c432b55b5b5 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts @@ -6,7 +6,11 @@ import { formatDate, normalizeAndFormatNationalId, } from '@island.is/judicial-system/formatters' -import { DateType, DefenderChoice } from '@island.is/judicial-system/types' +import { + DateType, + DefenderChoice, + isSuccessfulServiceStatus, +} from '@island.is/judicial-system/types' import { InternalCaseResponse } from './internal/internalCase.response' import { Groups } from './shared/groups.model' @@ -59,6 +63,12 @@ class SubpoenaData { @ApiProperty({ type: Boolean }) hasBeenServed?: boolean + + @ApiProperty({ type: Boolean }) + hasChosenDefender?: boolean + + @ApiProperty({ enum: DefenderChoice }) + defaultDefenderChoice?: DefenderChoice } export class SubpoenaResponse { @@ -87,9 +97,11 @@ export class SubpoenaResponse { ) const waivedRight = defendantInfo?.defenderChoice === DefenderChoice.WAIVE - const hasDefender = defendantInfo?.defenderName !== undefined - const subpoena = defendantInfo?.subpoenas ?? [] - const hasBeenServed = subpoena[0]?.acknowledged ?? false + const hasDefender = defendantInfo?.defenderName !== null + const subpoenas = defendantInfo?.subpoenas ?? [] + const hasBeenServed = + subpoenas.length > 0 && + isSuccessfulServiceStatus(subpoenas[0].serviceStatus) const canChangeDefenseChoice = !waivedRight && !hasDefender const subpoenaDateLog = internalCase.dateLogs?.find( @@ -108,6 +120,11 @@ export class SubpoenaResponse { title: t.subpoena, subtitle: courtNameAndAddress, hasBeenServed: hasBeenServed, + hasChosenDefender: Boolean( + defendantInfo?.defenderChoice && + defendantInfo.defenderChoice !== DefenderChoice.DELAY, + ), + defaultDefenderChoice: DefenderChoice.DELAY, alerts: [ ...(hasBeenServed ? [ diff --git a/apps/judicial-system/web/messages/Core/errors.ts b/apps/judicial-system/web/messages/Core/errors.ts index 3123d4e8745c..2f3a6f63c441 100644 --- a/apps/judicial-system/web/messages/Core/errors.ts +++ b/apps/judicial-system/web/messages/Core/errors.ts @@ -145,4 +145,16 @@ export const errors = defineMessages({ defaultMessage: 'Upp kom villa við að opna skjal', description: 'Notaður sem villuskilaboð þegar ekki gengur að opna skjal', }, + getSubpoenaStatusTitle: { + id: 'judicial.system.core:errors.get_subpoena_status_title', + defaultMessage: 'Ekki tókst að sækja stöðu birtingar', + description: + 'Notaður sem villuskilaboð þegar tekst að sækja stöðu birtingar', + }, + getSubpoenaStatus: { + id: 'judicial.system.core:errors.get_subpoena_status', + defaultMessage: 'Vinsamlegast reyndu aftur síðar', + description: + 'Notaður sem villuskilaboð þegar tekst að sækja stöðu birtingar', + }, }) diff --git a/apps/judicial-system/web/pages/domur/akaera/malflytjendur/[id].ts b/apps/judicial-system/web/pages/domur/akaera/malflytjendur/[id].ts index 4bf36a18f264..e686009b01a2 100644 --- a/apps/judicial-system/web/pages/domur/akaera/malflytjendur/[id].ts +++ b/apps/judicial-system/web/pages/domur/akaera/malflytjendur/[id].ts @@ -1,3 +1,3 @@ -import HearingArrangements from '@island.is/judicial-system-web/src/routes/Court/Indictments/Defender/Defender' +import Advocates from '@island.is/judicial-system-web/src/routes/Court/Indictments/Advocates/Advocates' -export default HearingArrangements +export default Advocates diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index 64acb88c88c8..d45521ec30d9 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -29,6 +29,13 @@ query Case($input: CaseQueryInput!) { subpoenas { id created + serviceStatus + serviceDate + servedBy + comment + defenderNationalId + caseId + subpoenaId } } defenderName diff --git a/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx b/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx index 10ffb585cef0..b737255e67c0 100644 --- a/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx +++ b/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx @@ -77,7 +77,7 @@ const InputNationalId: FC = (props) => { useEffect(() => { setErrorMessage(undefined) setInputValue(value ?? '') - }, [value]) + }, [value, isDateOfBirth]) return ( { + switch (serviceStatus) { + case ServiceStatus.DEFENDER: + case ServiceStatus.ELECTRONICALLY: + case ServiceStatus.IN_PERSON: + return strings.serviceStatusSuccess + case ServiceStatus.EXPIRED: + return strings.serviceStatusExpired + case ServiceStatus.FAILED: + return strings.serviceStatusFailed + // Should not happen + default: + return strings.serviceStatusUnknown + } +} + +const mapServiceStatusMessages = ( + subpoena: Subpoena, + formatMessage: IntlShape['formatMessage'], + lawyer?: Lawyer, +) => { + switch (subpoena.serviceStatus) { + case ServiceStatus.DEFENDER: + return [ + `${subpoena.servedBy} - ${formatDate(subpoena.serviceDate, 'Pp')}`, + formatMessage(strings.servedToDefender, { + lawyerName: lawyer?.name, + practice: lawyer?.practice, + }), + ] + case ServiceStatus.ELECTRONICALLY: + return [ + formatMessage(strings.servedToElectronically, { + date: formatDate(subpoena.serviceDate, 'Pp'), + }), + ] + case ServiceStatus.IN_PERSON: + case ServiceStatus.FAILED: + return [ + `${subpoena.servedBy} - ${formatDate(subpoena.serviceDate, 'Pp')}`, + subpoena.comment, + ] + case ServiceStatus.EXPIRED: + return [formatMessage(strings.serviceStatusExpiredMessage)] + default: + return [] + } +} + +const renderError = (formatMessage: IntlShape['formatMessage']) => ( + +) + +interface ServiceAnnouncementProps { + subpoena: Subpoena + defendantName?: string | null +} + +const ServiceAnnouncement: FC = (props) => { + const { subpoena: localSubpoena, defendantName } = props + + const [subpoena, setSubpoena] = useState() + + const { subpoenaStatus, subpoenaStatusLoading, subpoenaStatusError } = + useSubpoena(localSubpoena.caseId, localSubpoena.subpoenaId) + + const { formatMessage } = useIntl() + + const lawyer = useGetLawyer( + subpoena?.defenderNationalId, + subpoena?.serviceStatus === ServiceStatus.DEFENDER, + ) + + const title = mapServiceStatusTitle(subpoena?.serviceStatus) + const messages = subpoena + ? mapServiceStatusMessages(subpoena, formatMessage, lawyer) + : [] + + // Use data from RLS but fallback to local data + useEffect(() => { + if (subpoenaStatusError) { + setSubpoena(localSubpoena) + } else { + setSubpoena({ + ...localSubpoena, + servedBy: subpoenaStatus?.subpoenaStatus?.servedBy, + serviceStatus: subpoenaStatus?.subpoenaStatus?.serviceStatus, + serviceDate: subpoenaStatus?.subpoenaStatus?.serviceDate, + comment: subpoenaStatus?.subpoenaStatus?.comment, + defenderNationalId: subpoenaStatus?.subpoenaStatus?.defenderNationalId, + }) + } + }, [localSubpoena, subpoenaStatus, subpoenaStatusError]) + + return !defendantName ? null : !subpoena && !subpoenaStatusLoading ? ( + {renderError(formatMessage)} + ) : subpoenaStatusLoading ? ( + + + + ) : ( + + + {messages.map((msg) => ( + + {msg} + + ))} + + } + type={ + subpoena?.serviceStatus === ServiceStatus.FAILED || + subpoena?.serviceStatus === ServiceStatus.EXPIRED + ? 'warning' + : 'success' + } + /> + + ) +} + +export default ServiceAnnouncement diff --git a/apps/judicial-system/web/src/components/index.ts b/apps/judicial-system/web/src/components/index.ts index fa0cfce3c06f..85c50b4f5967 100644 --- a/apps/judicial-system/web/src/components/index.ts +++ b/apps/judicial-system/web/src/components/index.ts @@ -62,6 +62,7 @@ export { default as RestrictionTags } from './RestrictionTags/RestrictionTags' export { default as RulingAccordionItem } from './AccordionItems/RulingAccordionItem/RulingAccordionItem' export { default as RulingInput } from './RulingInput/RulingInput' export { default as SectionHeading } from './SectionHeading/SectionHeading' +export { default as ServiceAnnouncement } from './ServiceAnnouncement/ServiceAnnouncement' export { default as ServiceInterruptionBanner } from './ServiceInterruptionBanner/ServiceInterruptionBanner' export { default as SignedDocument } from './SignedDocument/SignedDocument' export { default as OverviewHeader } from './OverviewHeader/OverviewHeader' diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/Advocates.strings.ts b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/Advocates.strings.ts new file mode 100644 index 000000000000..1250e9fa97b9 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/Advocates.strings.ts @@ -0,0 +1,77 @@ +import { defineMessages } from 'react-intl' + +export const strings = defineMessages({ + title: { + id: 'judicial.system.core:court_indictments.advocates.title', + defaultMessage: 'Verjandi', + description: + 'Notaður sem titill á síðu á verjenda skrefi í dómaraflæði í ákærum.', + }, + alertBannerText: { + id: 'judicial.system.core:court_indictments.advocates.alert_banner_text', + defaultMessage: + 'Verjendur í sakamálum fá tilkynningu um skráningu í tölvupósti, aðgang að gögnum málsins og boð í þingfestingu.', + description: + 'Notaður sem texti í alert banner á málflytjendurskjá í ákærum.', + }, + selectDefenderHeading: { + id: 'judicial.system.core:court_indictments.advocates.select_defender_heading', + defaultMessage: 'Verjandi', + description: 'Notaður sem texti fyrir val á skipaðan verjanda í ákærum.', + }, + defendantWaivesRightToCounsel: { + id: 'judicial.system.core:court_indictments.advocates.defendant_waives_right_to_counsel', + defaultMessage: '{accused} óskar ekki eftir að sér sé skipaður verjandi', + description: + 'Notaður sem texti fyrir takka þegar ákærðu óska ekki eftir verjanda í dómaraflæði í ákærum. ', + }, + civilClaimants: { + id: 'judicial.system.core:court_indictments.advocates.civil_claimants', + defaultMessage: 'Kröfuhafar', + description: + 'Notaður sem titill á texta um kröfuhafa í dómaraflæði í ákærum.', + }, + shareFilesWithCivilClaimantAdvocate: { + id: 'judicial.system.core:court_indictments.advocates.civil_claimant_share_files_with_advocate', + defaultMessage: + 'Deila gögnum með {defenderIsLawyer, select, true {lögmanni} other {réttargæslumanni}} kröfuhafa', + description: 'Notaður sem texti á deila kröfum með kröfuhafa takka.', + }, + shareFilesWithCivilClaimantAdvocateTooltip: { + id: 'judicial.system.core:court_indictments.advocates.civil_claimant_share_files_with_advocate_tooltip', + defaultMessage: + 'Ef hakað er í þennan reit fær {defenderIsLawyer, select, true {lögmaður} other {réttargæslumaður}} kröfuhafa aðgang að gögnum málsins', + description: + 'Notaður sem texti í tooltip á deila kröfum með kröfuhafa takka.', + }, + lawyer: { + id: 'judicial.system.core:court_indictments.advocates.lawyer', + defaultMessage: 'Lögmaður', + description: 'Notaður sem texti fyrir lögmann í dómaraflæði í ákærum.', + }, + legalRightsProtector: { + id: 'judicial.system.core:court_indictments.advocates.legal_rights_protector', + defaultMessage: 'Réttargæslumaður', + description: + 'Notaður sem texti fyrir réttargæslumann í dómaraflæði í ákærum.', + }, + removeCivilClaimantAdvocate: { + id: 'judicial.system.core:court_indictments.advocates.remove_civil_claimant_advocate', + defaultMessage: + 'Fjarlægja {defenderIsLawyer, select, true {lögmann} other {réttargæslumann}}', + description: + 'Notaður sem texti fyrir eyða kröfuhafa í dómaraflæði í ákærum.', + }, + addCivilClaimantAdvocate: { + id: 'judicial.system.core:court_indictments.advocates.add_civil_claimant', + defaultMessage: 'Bæta við lögmanni kröfuhafa', + description: + 'Notaður sem texti fyrir bæta við kröfuhafa takka í dómaraflæði í ákærum.', + }, + noCivilClaimantAdvocate: { + id: 'judicial.system.core:court_indictments.advocates.no_civil_claimant_advocate', + defaultMessage: 'Enginn lögmaður skráður', + description: + 'Notaður sem texti þegar enginn lögmaður er skráður í dómaraflæði í ákærum.', + }, +}) diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/Defender.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/Advocates.tsx similarity index 74% rename from apps/judicial-system/web/src/routes/Court/Indictments/Defender/Defender.tsx rename to apps/judicial-system/web/src/routes/Court/Indictments/Advocates/Advocates.tsx index a629abf8760a..ffdba0747338 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/Defender.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/Advocates.tsx @@ -20,10 +20,11 @@ import { NotificationType } from '@island.is/judicial-system-web/src/graphql/sch import { useCase } from '@island.is/judicial-system-web/src/utils/hooks' import { isDefenderStepValid } from '@island.is/judicial-system-web/src/utils/validate' +import SelectCivilClaimantAdvocate from './SelectCivilClaimantAdvocate' import SelectDefender from './SelectDefender' -import { defender as m } from './Defender.strings' +import { strings } from './Advocates.strings' -const HearingArrangements = () => { +const Advocates = () => { const { workingCase, isLoadingWorkingCase, caseNotFound } = useContext(FormContext) const router = useRouter() @@ -39,6 +40,7 @@ const HearingArrangements = () => { ) const stepIsValid = !isSendingNotification && isDefenderStepValid(workingCase) + const hasCivilClaimants = (workingCase.civilClaimants?.length ?? 0) > 0 return ( { > - {formatMessage(m.title)} + {formatMessage(strings.title)} - + {workingCase.defendants?.map((defendant, index) => ( ))} + {hasCivilClaimants && ( + + + {workingCase.civilClaimants?.map((civilClaimant) => ( + + + + ))} + + )} { ) } -export default HearingArrangements +export default Advocates diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/SelectCivilClaimantAdvocate.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/SelectCivilClaimantAdvocate.tsx new file mode 100644 index 000000000000..c79fb2078f01 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/SelectCivilClaimantAdvocate.tsx @@ -0,0 +1,162 @@ +import { FC, useContext } from 'react' +import { useIntl } from 'react-intl' + +import { + AlertMessage, + Box, + Button, + Checkbox, + RadioButton, + Text, +} from '@island.is/island-ui/core' +import { + BlueBox, + FormContext, + InputAdvocate, +} from '@island.is/judicial-system-web/src/components' +import { + CivilClaimant, + UpdateCivilClaimantInput, +} from '@island.is/judicial-system-web/src/graphql/schema' +import { useCivilClaimants } from '@island.is/judicial-system-web/src/utils/hooks' + +import { strings } from './Advocates.strings' + +interface Props { + civilClaimant: CivilClaimant +} + +const SelectCivilClaimantAdvocate: FC = ({ civilClaimant }) => { + const { setAndSendCivilClaimantToServer } = useCivilClaimants() + const { workingCase, setWorkingCase } = useContext(FormContext) + + const { formatMessage } = useIntl() + + const updateCivilClaimant = (update: UpdateCivilClaimantInput) => { + setAndSendCivilClaimantToServer( + { + ...update, + caseId: workingCase.id, + civilClaimantId: civilClaimant.id, + }, + setWorkingCase, + ) + } + + return ( + + + {civilClaimant.name} + + {civilClaimant.hasSpokesperson ? ( + <> + + + + updateCivilClaimant({ + spokespersonIsLawyer: true, + } as UpdateCivilClaimantInput) + } + /> + + + + updateCivilClaimant({ + spokespersonIsLawyer: false, + } as UpdateCivilClaimantInput) + } + /> + + + + + + { + updateCivilClaimant({ + caseFilesSharedWithSpokesperson: + !civilClaimant.caseFilesSharedWithSpokesperson, + } as UpdateCivilClaimantInput) + }} + tooltip={formatMessage( + strings.shareFilesWithCivilClaimantAdvocateTooltip, + { + defenderIsLawyer: civilClaimant.spokespersonIsLawyer, + }, + )} + backgroundColor="white" + large + filled + /> + + ) : ( + + )} + + + + + ) +} + +export default SelectCivilClaimantAdvocate diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/SelectDefender.tsx similarity index 96% rename from apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx rename to apps/judicial-system/web/src/routes/Court/Indictments/Advocates/SelectDefender.tsx index 824cca080df2..15b455791e48 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Advocates/SelectDefender.tsx @@ -16,7 +16,7 @@ import { } from '@island.is/judicial-system-web/src/graphql/schema' import { useDefendants } from '@island.is/judicial-system-web/src/utils/hooks' -import { defender as m } from './Defender.strings' +import { strings } from './Advocates.strings' interface Props { defendant: Defendant @@ -80,7 +80,7 @@ const SelectDefender: FC = ({ defendant }) => { dataTestId={`defendantWaivesRightToCounsel-${defendant.id}`} name={`defendantWaivesRightToCounsel-${defendant.id}`} label={capitalize( - formatMessage(m.defendantWaivesRightToCounsel, { + formatMessage(strings.defendantWaivesRightToCounsel, { accused: formatMessage(core.indictmentDefendant, { gender }), }), )} diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/Defender.strings.ts b/apps/judicial-system/web/src/routes/Court/Indictments/Defender/Defender.strings.ts deleted file mode 100644 index 2c6984795367..000000000000 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/Defender.strings.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defineMessages } from 'react-intl' - -export const defender = defineMessages({ - title: { - id: 'judicial.system.core:court_indictments.defender.title_v1', - defaultMessage: 'Verjandi', - description: - 'Notaður sem titill á síðu á verjenda skrefi í dómaraflæði í ákærum.', - }, - alertBannerText: { - id: 'judicial.system.core:court_indictments.defender.alert_banner_text_v1', - defaultMessage: - 'Verjendur í sakamálum fá tilkynningu um skráningu í tölvupósti, aðgang að gögnum málsins og boð í þingfestingu.', - description: - 'Notaður sem texti í alert banner á málflytjendurskjá í ákærum.', - }, - selectDefenderHeading: { - id: 'judicial.system.core:court_indictments.defender.select_defender_heading_v1', - defaultMessage: 'Verjandi', - description: 'Notaður sem texti fyrir val á skipaðan verjanda í ákærum.', - }, - defendantWaivesRightToCounsel: { - id: 'judicial.system.core:court_indictments.defender.defendant_waives_right_to_counsel', - defaultMessage: '{accused} óskar ekki eftir að sér sé skipaður verjandi', - description: - 'Notaður sem texti fyrir takka þegar ákærðu óska ekki eftir verjanda í dómaraflæði í ákærum. ', - }, -}) 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 176c08156d9e..bf1f5cfedb8a 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 @@ -18,6 +18,7 @@ import { PageHeader, PageLayout, PageTitle, + ServiceAnnouncement, useIndictmentsLawsBroken, } from '@island.is/judicial-system-web/src/components' import { IndictmentDecision } from '@island.is/judicial-system-web/src/graphql/schema' @@ -39,6 +40,7 @@ const IndictmentOverview = () => { const latestDate = workingCase.courtDate ?? workingCase.arraignmentDate const isArraignmentScheduled = Boolean(workingCase.arraignmentDate) + // const caseHasBeenReceivedByCourt = workingCase.state === CaseState.RECEIVED const handleNavigationTo = useCallback( @@ -76,6 +78,18 @@ const IndictmentOverview = () => { {formatMessage(strings.inProgressTitle)} + {workingCase.defendants?.map((defendant) => + defendant.subpoenas?.map( + (subpoena) => + subpoena.subpoenaId && ( + + ), + ), + )} {workingCase.court && latestDate?.date && workingCase.indictmentDecision !== IndictmentDecision.COMPLETING && diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx index 3c13b47bef12..fc8d3994709c 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx @@ -29,6 +29,7 @@ import { PageLayout, ProsecutorCaseInfo, SectionHeading, + ServiceAnnouncement, useIndictmentsLawsBroken, UserContext, } from '@island.is/judicial-system-web/src/components' @@ -193,6 +194,15 @@ const Overview: FC = () => { + {workingCase.defendants?.map((defendant) => + defendant.subpoenas?.map((subpoena) => ( + + )), + )} {workingCase.court && latestDate?.date && workingCase.indictmentDecision !== IndictmentDecision.COMPLETING && diff --git a/apps/judicial-system/web/src/utils/hooks/index.ts b/apps/judicial-system/web/src/utils/hooks/index.ts index 3c9d8a29cec0..cc4edd7be94a 100644 --- a/apps/judicial-system/web/src/utils/hooks/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/index.ts @@ -29,3 +29,4 @@ export { default as useSections } from './useSections' export { default as useCaseList } from './useCaseList' export { default as useNationalRegistry } from './useNationalRegistry' export { default as useCivilClaimants } from './useCivilClaimants' +export { default as useSubpoena } from './useSubpoena' diff --git a/apps/judicial-system/web/src/utils/hooks/useSubpoena/getSubpoenaStatus.graphql b/apps/judicial-system/web/src/utils/hooks/useSubpoena/getSubpoenaStatus.graphql new file mode 100644 index 000000000000..a87df3e15366 --- /dev/null +++ b/apps/judicial-system/web/src/utils/hooks/useSubpoena/getSubpoenaStatus.graphql @@ -0,0 +1,9 @@ +query SubpoenaStatus($input: SubpoenaStatusQueryInput!) { + subpoenaStatus(input: $input) { + serviceStatus + servedBy + comment + serviceDate + defenderNationalId + } +} diff --git a/apps/judicial-system/web/src/utils/hooks/useSubpoena/index.ts b/apps/judicial-system/web/src/utils/hooks/useSubpoena/index.ts new file mode 100644 index 000000000000..3d2ab133f847 --- /dev/null +++ b/apps/judicial-system/web/src/utils/hooks/useSubpoena/index.ts @@ -0,0 +1,20 @@ +import { useSubpoenaStatusQuery } from './getSubpoenaStatus.generated' + +const useSubpoena = (caseId?: string | null, subpoenaId?: string | null) => { + const { + data: subpoenaStatus, + loading: subpoenaStatusLoading, + error: subpoenaStatusError, + } = useSubpoenaStatusQuery({ + variables: { + input: { + caseId: caseId ?? '', + subpoenaId: subpoenaId ?? '', + }, + }, + }) + + return { subpoenaStatus, subpoenaStatusLoading, subpoenaStatusError } +} + +export default useSubpoena 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 77da007af1cf..202e2dbaf644 100644 --- a/apps/judicial-system/xrd-api/src/app/app.service.ts +++ b/apps/judicial-system/xrd-api/src/app/app.service.ts @@ -16,7 +16,7 @@ import { 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 { DefenderChoice, ServiceStatus } from '@island.is/judicial-system/types' import { UpdateSubpoenaDto } from './dto/subpoena.dto' import { SubpoenaResponse } from './models/subpoena.response' @@ -96,35 +96,69 @@ export class AppService { subpoenaId: string, updateSubpoena: UpdateSubpoenaDto, ): Promise { - let update = { ...updateSubpoena } + let defenderInfo: { + defenderName: string | undefined + defenderEmail: string | undefined + defenderPhoneNumber: string | undefined + } = { + defenderName: undefined, + defenderEmail: undefined, + defenderPhoneNumber: undefined, + } if ( - update.defenderChoice === DefenderChoice.CHOOSE && - !update.defenderNationalId + updateSubpoena.defenderChoice === DefenderChoice.CHOOSE && + !updateSubpoena.defenderNationalId ) { throw new BadRequestException( 'Defender national id is required for choice', ) } - if (update.defenderNationalId) { + if (updateSubpoena.defenderNationalId) { try { const chosenLawyer = await this.lawyersService.getLawyer( - update.defenderNationalId, + updateSubpoena.defenderNationalId, ) - update = { - ...update, - ...{ - defenderName: chosenLawyer.Name, - defenderEmail: chosenLawyer.Email, - defenderPhoneNumber: chosenLawyer.Phone, - }, + defenderInfo = { + defenderName: chosenLawyer.Name, + defenderEmail: chosenLawyer.Email, + defenderPhoneNumber: chosenLawyer.Phone, } } catch (reason) { + // TODO: Reconsider throwing - what happens if registry is down? + this.logger.error( + `Failed to retrieve lawyer with national id ${updateSubpoena.defenderNationalId}`, + reason, + ) throw new BadRequestException('Lawyer not found') } } + //TODO: move logic to reusable place if this is the data structure we keep + const serviceStatus = updateSubpoena.deliveredToLawyer + ? ServiceStatus.DEFENDER + : updateSubpoena.prosecutedConfirmedSubpoenaThroughIslandis + ? ServiceStatus.ELECTRONICALLY + : updateSubpoena.deliveredOnPaper || updateSubpoena.delivered === true + ? ServiceStatus.IN_PERSON + : updateSubpoena.acknowledged === false + ? ServiceStatus.FAILED + : // TODO: handle expired + undefined + + const updateToSend = { + serviceStatus, + comment: updateSubpoena.comment, + servedBy: updateSubpoena.servedBy, + serviceDate: updateSubpoena.servedAt, + defenderChoice: updateSubpoena.defenderChoice, + defenderNationalId: updateSubpoena.defenderNationalId, + defenderName: defenderInfo.defenderName, + defenderEmail: defenderInfo.defenderEmail, + defenderPhoneNumber: defenderInfo.defenderPhoneNumber, + } + try { const res = await fetch( `${this.config.backend.url}/api/internal/subpoena/${subpoenaId}`, @@ -134,7 +168,7 @@ export class AppService { 'Content-Type': 'application/json', authorization: `Bearer ${this.config.backend.accessToken}`, }, - body: JSON.stringify(update), + body: JSON.stringify(updateToSend), }, ) 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 index 7ac2807b0467..edb6ff7db4d6 100644 --- a/apps/judicial-system/xrd-api/src/app/dto/subpoena.dto.ts +++ b/apps/judicial-system/xrd-api/src/app/dto/subpoena.dto.ts @@ -18,7 +18,12 @@ export class UpdateSubpoenaDto { @IsOptional() @IsString() @ApiProperty({ type: String, required: false }) - registeredBy?: string + servedBy?: string + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + servedAt?: string @IsOptional() @IsEnum(DefenderChoice) @@ -29,4 +34,24 @@ export class UpdateSubpoenaDto { @IsString() @ApiProperty({ type: String, required: false }) defenderNationalId?: string + + @IsOptional() + @IsBoolean() + @ApiProperty({ type: Boolean, required: false }) + prosecutedConfirmedSubpoenaThroughIslandis?: boolean + + @IsOptional() + @IsBoolean() + @ApiProperty({ type: Boolean, required: false }) + delivered?: boolean + + @IsOptional() + @IsBoolean() + @ApiProperty({ type: Boolean, required: false }) + deliveredOnPaper?: boolean + + @IsOptional() + @IsBoolean() + @ApiProperty({ type: Boolean, required: false }) + deliveredToLawyer?: boolean } diff --git a/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj b/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj index b13fc775ca43..a6c26ffd0c01 100644 --- a/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj +++ b/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj @@ -550,7 +550,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; @@ -622,7 +625,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; @@ -742,7 +748,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/apps/native/app/ios/Podfile.lock b/apps/native/app/ios/Podfile.lock index d751cd655a22..33c82a8973d9 100644 --- a/apps/native/app/ios/Podfile.lock +++ b/apps/native/app/ios/Podfile.lock @@ -33,7 +33,7 @@ PODS: - ExpoModulesCore - ExpoFileSystem (17.0.1): - ExpoModulesCore - - ExpoFont (12.0.9): + - ExpoFont (12.0.10): - ExpoModulesCore - ExpoHaptics (13.0.1): - ExpoModulesCore @@ -1923,7 +1923,7 @@ SPEC CHECKSUMS: Expo: 88047e6d12a8113a18887b6ebd775fccfcdbf3c9 ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875 ExpoFileSystem: 80bfe850b1f9922c16905822ecbf97acd711dc51 - ExpoFont: e7f2275c10ca8573c991e007329ad6bf98086485 + ExpoFont: 00756e6c796d8f7ee8d211e29c8b619e75cbf238 ExpoHaptics: 5a3a88971af384255baf2504f38b41189cec6984 ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08 ExpoLocalAuthentication: 9e02a56a4cf9868f0052656a93d4c94101a42ed7 diff --git a/apps/native/app/package.json b/apps/native/app/package.json index fdcf1cede4ab..c7319cdd1cdb 100644 --- a/apps/native/app/package.json +++ b/apps/native/app/package.json @@ -52,6 +52,7 @@ "@react-native/metro-config": "0.74.87", "@react-native/typescript-config": "0.74.87", "apollo3-cache-persist": "0.15.0", + "compare-versions": "6.1.1", "configcat-js": "7.0.0", "dynamic-color": "0.3.0", "expo": "51.0.25", diff --git a/apps/native/app/src/assets/illustrations/digital-services-m1-dots.png b/apps/native/app/src/assets/illustrations/digital-services-m1-dots.png new file mode 100644 index 000000000000..12acfb1ff9f5 Binary files /dev/null and b/apps/native/app/src/assets/illustrations/digital-services-m1-dots.png differ diff --git a/apps/native/app/src/assets/illustrations/digital-services-m1-dots@2x.png b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@2x.png new file mode 100644 index 000000000000..f663e415ec12 Binary files /dev/null and b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@2x.png differ diff --git a/apps/native/app/src/assets/illustrations/digital-services-m1-dots@3x.png b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@3x.png new file mode 100644 index 000000000000..23a3b9b71ec9 Binary files /dev/null and b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@3x.png differ diff --git a/apps/native/app/src/graphql/client.ts b/apps/native/app/src/graphql/client.ts index fbac7b3719a1..2e4e29eb5d34 100644 --- a/apps/native/app/src/graphql/client.ts +++ b/apps/native/app/src/graphql/client.ts @@ -20,6 +20,7 @@ import { environmentStore } from '../stores/environment-store' import { createMMKVStorage } from '../stores/mmkv' import { offlineStore } from '../stores/offline-store' import { MainBottomTabs } from '../utils/component-registry' +import { getCustomUserAgent } from '../utils/user-agent' const apolloMMKVStorage = createMMKVStorage({ withEncryption: true }) @@ -134,6 +135,7 @@ const authLink = setContext(async (_, { headers }) => ({ 'X-Cognito-Token': `Bearer ${ environmentStore.getState().cognito?.accessToken }`, + 'User-Agent': getCustomUserAgent(), cookie: [authStore.getState().cookies] .filter((x) => String(x) !== '') .join('; '), diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 0662636e4777..845661117191 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -596,4 +596,11 @@ export const en: TranslatedMessages = { 'passkeys.skipButton': 'Skip', 'passkeys.errorRegistering': 'Error', 'passkeys.errorRegisteringMessage': 'Could not create a passkey', + + // update app + 'updateApp.title': 'Update app', + 'updateApp.description': + 'You are about to use an old version of the Island.is app. Please update the app to be able to continue.', + 'updateApp.button': 'Update', + 'updateApp.buttonSkip': 'Skip', } diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index 19e80cf23b38..303d26dfa3e0 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -595,4 +595,11 @@ export const is = { 'passkeys.skipButton': 'Sleppa', 'passkeys.errorRegistering': 'Villa', 'passkeys.errorRegisteringMessage': 'Tókst ekki að búa til aðgangslykil', + + // update app + 'updateApp.title': 'Uppfæra app', + 'updateApp.description': + 'Þú ert að fara að nota gamla útgáfu af Ísland.is appinu. Vinsamlegast uppfærðu appið til að halda áfram.', + 'updateApp.button': 'Uppfæra', + 'updateApp.buttonSkip': 'Sleppa', } diff --git a/apps/native/app/src/screens/applications/applications-completed.tsx b/apps/native/app/src/screens/applications/applications-completed.tsx index 1038c67b87f3..32ffb4bd29d8 100644 --- a/apps/native/app/src/screens/applications/applications-completed.tsx +++ b/apps/native/app/src/screens/applications/applications-completed.tsx @@ -44,7 +44,7 @@ export const ApplicationsCompletedScreen: NavigationFunctionComponent = ({ ApplicationResponseDtoStatusEnum.Approved, ], }, - locale: locale === 'is-US' ? 'is' : 'en', + locale: locale === 'is-IS' ? 'is' : 'en', }, }) diff --git a/apps/native/app/src/screens/applications/applications-in-progress.tsx b/apps/native/app/src/screens/applications/applications-in-progress.tsx index 08c1c7f3d47e..28305f7fb516 100644 --- a/apps/native/app/src/screens/applications/applications-in-progress.tsx +++ b/apps/native/app/src/screens/applications/applications-in-progress.tsx @@ -40,7 +40,7 @@ export const ApplicationsInProgressScreen: NavigationFunctionComponent = ({ input: { status: [ApplicationResponseDtoStatusEnum.Inprogress], }, - locale: locale === 'is-US' ? 'is' : 'en', + locale: locale === 'is-IS' ? 'is' : 'en', }, }) diff --git a/apps/native/app/src/screens/applications/applications-incomplete.tsx b/apps/native/app/src/screens/applications/applications-incomplete.tsx index 70874676aed1..d9a224e4e667 100644 --- a/apps/native/app/src/screens/applications/applications-incomplete.tsx +++ b/apps/native/app/src/screens/applications/applications-incomplete.tsx @@ -40,7 +40,7 @@ export const ApplicationsIncompleteScreen: NavigationFunctionComponent = ({ input: { status: [ApplicationResponseDtoStatusEnum.Draft], }, - locale: locale === 'is-US' ? 'is' : 'en', + locale: locale === 'is-IS' ? 'is' : 'en', }, }) diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx index 07106e266c41..057b658de585 100644 --- a/apps/native/app/src/screens/home/home.tsx +++ b/apps/native/app/src/screens/home/home.tsx @@ -28,8 +28,10 @@ import { } from '../../stores/preferences-store' import { useUiStore } from '../../stores/ui-store' import { isAndroid } from '../../utils/devices' +import { needsToUpdateAppVersion } from '../../utils/minimum-app-version' import { getRightButtons } from '../../utils/get-main-root' import { testIDs } from '../../utils/test-ids' +import { navigateTo } from '../../lib/deep-linking' import { AirDiscountModule, useGetAirDiscountQuery, @@ -254,12 +256,21 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ const keyExtractor = useCallback((item: ListItem) => item.id, []) const scrollY = useRef(new Animated.Value(0)).current + const isAppUpdateRequired = useCallback(async () => { + const needsUpdate = await needsToUpdateAppVersion() + if (needsUpdate) { + navigateTo('/update-app', { closable: false }) + } + }, []) + useEffect(() => { // Sync push tokens and unseen notifications syncToken() checkUnseen() // Get user locale from server getAndSetLocale() + // Check if upgrade wall should be shown + isAppUpdateRequired() }, []) const refetch = useCallback(async () => { diff --git a/apps/native/app/src/screens/passkey/passkey.tsx b/apps/native/app/src/screens/passkey/passkey.tsx index 16aee81fce55..238648af275b 100644 --- a/apps/native/app/src/screens/passkey/passkey.tsx +++ b/apps/native/app/src/screens/passkey/passkey.tsx @@ -15,7 +15,7 @@ import { } from 'react-native-navigation' import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import logo from '../../assets/logo/logo-64w.png' -import illustrationSrc from '../../assets/illustrations/digital-services-m1.png' +import illustrationSrc from '../../assets/illustrations/digital-services-m1-dots.png' import { openNativeBrowser } from '../../lib/rn-island' import { preferencesStore } from '../../stores/preferences-store' import { useRegisterPasskey } from '../../lib/passkeys/useRegisterPasskey' @@ -31,6 +31,22 @@ const Text = styled.View` margin-top: ${({ theme }) => theme.spacing[5]}px; ` +const Host = styled.View` + justify-content: center; + align-items: center; + flex: 1; +` + +const ButtonWrapper = styled.View` + padding-horizontal: ${({ theme }) => theme.spacing[2]}px; + padding-vertical: ${({ theme }) => theme.spacing[4]}px; +` + +const Title = styled(Typography)` + padding-horizontal: ${({ theme }) => theme.spacing[2]}px; + margin-bottom: ${({ theme }) => theme.spacing[2]}px; +` + const LoadingOverlay = styled.View` flex: 1; justify-content: center; @@ -83,32 +99,19 @@ export const PasskeyScreen: NavigationFunctionComponent<{ style={{ marginHorizontal: 16 }} /> - + - + <FormattedMessage id="passkeys.headingTitle" defaultMessage="Innskrá með Ísland.is appinu" /> - </Typography> + - - + + {vehicle && ( - + )} diff --git a/apps/skilavottord/web/utils/dateUtils.ts b/apps/skilavottord/web/utils/dateUtils.ts index f154416896d7..9356d69faa87 100644 --- a/apps/skilavottord/web/utils/dateUtils.ts +++ b/apps/skilavottord/web/utils/dateUtils.ts @@ -20,6 +20,11 @@ export const getTime = (dateTime: string) => { } export const getYear = (dateTime: string) => { + // if null return empty string instead of 1970 + if (!dateTime) { + return '' + } + const date = new Date(dateTime) return format(date, 'yyyy') } diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/dto/createVehicle.input.ts b/apps/skilavottord/ws/src/app/modules/vehicle/dto/createVehicle.input.ts index 3878eb285e10..077ca1cb4f53 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/dto/createVehicle.input.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/dto/createVehicle.input.ts @@ -14,8 +14,8 @@ export class CreateVehicleInput { @Field() make!: string - @Field() - firstRegistrationDate!: Date + @Field({ nullable: true }) + firstRegistrationDate: Date @Field() color!: string diff --git a/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts b/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts index a5974aef207c..02a1294a6276 100644 --- a/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts +++ b/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts @@ -68,9 +68,9 @@ test.describe.serial('Custody tests', () => { await page.locator('#defendantGender').click() await page.locator('#react-select-defendantGender-option-0').click() await page - .locator('input[id=react-select-defenderName-input]') + .locator('input[id=react-select-advocateName-input]') .fill('Saul Goodman') - await page.locator('#react-select-defenderName-option-0').click() + await page.locator('#react-select-advocateName-option-0').click() await page .locator('input[name=defenderEmail]') .fill('jl+auto+defender@kolibri.is') diff --git a/apps/web/components/Organization/Slice/Districts/DistrictsSlice.css.ts b/apps/web/components/Organization/Slice/Districts/DistrictsSlice.css.ts index fa6b26b2d28b..a6e2e0787f22 100644 --- a/apps/web/components/Organization/Slice/Districts/DistrictsSlice.css.ts +++ b/apps/web/components/Organization/Slice/Districts/DistrictsSlice.css.ts @@ -1,4 +1,5 @@ import { style } from '@vanilla-extract/css' + import { themeUtils } from '@island.is/island-ui/theme' export const districtsList = style({ @@ -18,3 +19,8 @@ export const districtsList = style({ }, }), }) + +export const districtsListItem = style({ + breakInside: 'avoid', + display: 'block', +}) diff --git a/apps/web/components/Organization/Slice/Districts/DistrictsSlice.tsx b/apps/web/components/Organization/Slice/Districts/DistrictsSlice.tsx index e35c7fd8b7ff..6186d4007323 100644 --- a/apps/web/components/Organization/Slice/Districts/DistrictsSlice.tsx +++ b/apps/web/components/Organization/Slice/Districts/DistrictsSlice.tsx @@ -37,7 +37,7 @@ export const DistrictsSlice: React.FC> = ({ - + {slice.description && ( {slice.description} @@ -50,7 +50,12 @@ export const DistrictsSlice: React.FC> = ({ className={styles.districtsList} > {slice.links.map((link, index) => ( - +