diff --git a/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts b/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts index 3b2982563026..6372ac2e17d2 100644 --- a/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts +++ b/apps/judicial-system/api/src/app/modules/case-list/models/caseList.model.ts @@ -77,6 +77,9 @@ export class CaseListEntry { @Field(() => String, { nullable: true }) readonly prosecutorPostponedAppealDate?: string + @Field(() => Institution, { nullable: true }) + readonly court?: Institution + @Field(() => User, { nullable: true }) readonly creatingProsecutor?: User diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index 6454603536c8..097e9c4d5b4a 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -409,6 +409,7 @@ export const include: Includeable[] = [ ] export const caseListInclude: Includeable[] = [ + { model: Institution, as: 'court' }, { model: Institution, as: 'prosecutorsOffice' }, { model: Defendant, @@ -775,12 +776,16 @@ export class CaseService { CaseFileCategory.CRIMINAL_RECORD, CaseFileCategory.COST_BREAKDOWN, CaseFileCategory.CASE_FILE, + CaseFileCategory.PROSECUTOR_CASE_FILE, + CaseFileCategory.DEFENDANT_CASE_FILE, ] : [ CaseFileCategory.INDICTMENT, CaseFileCategory.CRIMINAL_RECORD, CaseFileCategory.COST_BREAKDOWN, CaseFileCategory.CASE_FILE, + CaseFileCategory.PROSECUTOR_CASE_FILE, + CaseFileCategory.DEFENDANT_CASE_FILE, ] const deliverCaseFileToCourtMessages = diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts index dbc56797bcb1..bee2da1a9852 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts @@ -23,7 +23,6 @@ export class CaseListInterceptor implements NestInterceptor { // WARNING: Be careful when adding to this list. No sensitive information should be returned. // If you need to add sensitive information, then you should consider adding a new endpoint // for defenders and other user roles that are not allowed to see sensitive information. - return { id: theCase.id, created: theCase.created, @@ -65,6 +64,7 @@ export class CaseListInterceptor implements NestInterceptor { indictmentRulingDecision: theCase.indictmentRulingDecision, courtSessionType: theCase.courtSessionType, eventLogs: theCase.eventLogs, + court: theCase.court, } }), ), diff --git a/apps/judicial-system/backend/src/app/modules/file/file.service.ts b/apps/judicial-system/backend/src/app/modules/file/file.service.ts index 397f488a5c0c..fbf3b50a9d5e 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.service.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.service.ts @@ -148,12 +148,12 @@ export class FileService { courtDocumentFolder = CourtDocumentFolder.INDICTMENT_DOCUMENTS break case CaseFileCategory.COURT_RECORD: - courtDocumentFolder = CourtDocumentFolder.COURT_DOCUMENTS - break case CaseFileCategory.RULING: courtDocumentFolder = CourtDocumentFolder.COURT_DOCUMENTS break case CaseFileCategory.CASE_FILE: + case CaseFileCategory.PROSECUTOR_CASE_FILE: + case CaseFileCategory.DEFENDANT_CASE_FILE: case undefined: case null: courtDocumentFolder = CourtDocumentFolder.CASE_DOCUMENTS @@ -388,6 +388,25 @@ export class FileService { }, ]) } + + if ( + isIndictmentCase(theCase.type) && + file.category && + [ + CaseFileCategory.PROSECUTOR_CASE_FILE, + CaseFileCategory.DEFENDANT_CASE_FILE, + ].includes(file.category) + ) { + await this.messageService.sendMessagesToQueue([ + { + type: MessageType.DELIVERY_TO_COURT_CASE_FILE, + user, + caseId: theCase.id, + elementId: file.id, + }, + ]) + } + return file } diff --git a/apps/judicial-system/web/messages/Core/tables.ts b/apps/judicial-system/web/messages/Core/tables.ts index 66879b64ce6a..b2b2500d65bb 100644 --- a/apps/judicial-system/web/messages/Core/tables.ts +++ b/apps/judicial-system/web/messages/Core/tables.ts @@ -134,4 +134,15 @@ export const tables = defineMessages({ defaultMessage: 'Sent', description: 'Notaður sem titill fyrir sent dálk í lista yfir mál.', }, + fineTag: { + id: 'judicial.system.core:tables.fine_tag', + defaultMessage: 'Viðurlagaákvörðun', + description: + 'Notaðir sem texti í tagg þegar mál endar sem viðurlagaákvörðun', + }, + rulingTag: { + id: 'judicial.system.core:tables.ruling_tag', + defaultMessage: 'Dómur', + description: 'Notaðir sem texti í tagg þegar mál endar sem dómur', + }, }) diff --git a/apps/judicial-system/web/src/components/Table/Table.tsx b/apps/judicial-system/web/src/components/Table/Table.tsx index 45b6398dfe04..8e572422391c 100644 --- a/apps/judicial-system/web/src/components/Table/Table.tsx +++ b/apps/judicial-system/web/src/components/Table/Table.tsx @@ -6,7 +6,10 @@ import { AnimatePresence, motion } from 'framer-motion' import { Box, Text } from '@island.is/island-ui/core' import { theme } from '@island.is/island-ui/theme' -import { formatDate } from '@island.is/judicial-system/formatters' +import { + districtCourtAbbreviation, + formatDate, +} from '@island.is/judicial-system/formatters' import { CaseType, isCompletedCase, @@ -168,25 +171,30 @@ const Table: FC = (props) => { return null } + const getColumnValue = ( + entry: CaseListEntry, + column: keyof CaseListEntry, + ) => { + const courtAbbreviation = districtCourtAbbreviation(entry.court?.name) + + switch (column) { + case 'defendants': + return entry.defendants?.[0]?.name ?? '' + case 'courtCaseNumber': + return courtAbbreviation + ? `${courtAbbreviation}: ${entry.courtCaseNumber}` + : entry.courtCaseNumber ?? '' + default: + return entry[column]?.toString() ?? '' + } + } + useMemo(() => { if (sortConfig) { data.sort((a: CaseListEntry, b: CaseListEntry) => { - const getColumnValue = (entry: CaseListEntry) => { - if ( - sortConfig.column === 'defendants' && - entry.defendants && - entry.defendants.length > 0 && - entry.defendants[0].name - ) { - return entry.defendants[0].name - } - - return entry[sortConfig.column]?.toString() - } - const compareResult = compareLocaleIS( - getColumnValue(a), - getColumnValue(b), + getColumnValue(a, sortConfig.column), + getColumnValue(b, sortConfig.column), ) return sortConfig.direction === 'ascending' diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesAwaitingReview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesAwaitingReview.tsx index 5f1cc2d3dac2..ec030bbaa80b 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesAwaitingReview.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesAwaitingReview.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' import { useIntl } from 'react-intl' import { AnimatePresence } from 'framer-motion' -import { Text } from '@island.is/island-ui/core' +import { Tag, Text } from '@island.is/island-ui/core' import { capitalize, formatDate } from '@island.is/judicial-system/formatters' import { core, tables } from '@island.is/judicial-system-web/messages' import { SectionHeading } from '@island.is/judicial-system-web/src/components' @@ -18,7 +18,10 @@ import TableInfoContainer from '@island.is/judicial-system-web/src/components/Ta import TagCaseState, { mapIndictmentCaseStateToTagVariant, } from '@island.is/judicial-system-web/src/components/TagCaseState/TagCaseState' -import { CaseListEntry } from '@island.is/judicial-system-web/src/graphql/schema' +import { + CaseIndictmentRulingDecision, + CaseListEntry, +} from '@island.is/judicial-system-web/src/graphql/schema' import { strings } from './CasesAwaitingReview.strings' @@ -48,6 +51,7 @@ const CasesForReview: FC = ({ loading, cases }) => { ), sortable: { isSortable: true, key: 'defendants' }, }, + { title: formatMessage(tables.type) }, { title: formatMessage(tables.state) }, { title: formatMessage(tables.deadline), @@ -58,9 +62,9 @@ const CasesForReview: FC = ({ loading, cases }) => { }, ]} data={cases} - generateContextMenuItems={(row) => { - return [openCaseInNewTabMenuItem(row.id)] - }} + generateContextMenuItems={(row) => [ + openCaseInNewTabMenuItem(row.id), + ]} columns={[ { cell: (row) => ( @@ -74,6 +78,18 @@ const CasesForReview: FC = ({ loading, cases }) => { { cell: (row) => , }, + { + cell: (row) => ( + + {formatMessage( + row.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE + ? tables.fineTag + : tables.rulingTag, + )} + + ), + }, { cell: (row) => ( = ({ loading, cases }) => { thead={[ { title: formatMessage(tables.caseNumber), + sortable: { + isSortable: true, + key: 'courtCaseNumber', + }, }, { title: capitalize( @@ -51,6 +62,7 @@ const CasesForReview: FC = ({ loading, cases }) => { key: 'defendants', }, }, + { title: formatMessage(tables.type) }, { title: formatMessage(tables.state) }, { title: formatMessage(tables.prosecutorName) }, { @@ -62,22 +74,42 @@ const CasesForReview: FC = ({ loading, cases }) => { }, ]} data={cases} - generateContextMenuItems={(row) => { - return [openCaseInNewTabMenuItem(row.id)] - }} + generateContextMenuItems={(row) => [ + openCaseInNewTabMenuItem(row.id), + ]} columns={[ { - cell: (row) => ( - - ), + cell: (row) => { + const courtAbbreviation = districtCourtAbbreviation( + row.court?.name, + ) + + return ( + + ) + }, }, { cell: (row) => , }, + { + cell: (row) => ( + + {formatMessage( + row.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE + ? tables.fineTag + : tables.rulingTag, + )} + + ), + }, { cell: (row) => ( = ({ loading, cases }) => { thead={[ { title: formatMessage(tables.caseNumber), + sortable: { + isSortable: true, + key: 'courtCaseNumber', + }, }, { title: capitalize( @@ -96,27 +103,48 @@ const CasesReviewed: FC = ({ loading, cases }) => { ), sortable: { isSortable: true, key: 'defendants' }, }, + { title: formatMessage(tables.type) }, { title: formatMessage(tables.reviewDecision) }, { title: formatMessage(tables.verdictViewState) }, { title: formatMessage(tables.prosecutorName) }, ]} data={cases} - generateContextMenuItems={(row) => { - return [openCaseInNewTabMenuItem(row.id)] - }} + generateContextMenuItems={(row) => [ + openCaseInNewTabMenuItem(row.id), + ]} columns={[ { - cell: (row) => ( - - ), + cell: (row) => { + const courtAbbreviation = districtCourtAbbreviation( + row.court?.name, + ) + + return ( + + ) + }, }, { cell: (row) => , }, + { + cell: (row) => ( + + {formatMessage( + row.indictmentRulingDecision === + CaseIndictmentRulingDecision.FINE + ? tables.fineTag + : tables.rulingTag, + )} + + ), + }, { cell: (row) => ( diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql b/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql index d35f7fb9af54..4001b83c6fcb 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql +++ b/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql @@ -34,6 +34,10 @@ query Cases { initialRulingDate rulingDate rulingSignatureDate + court { + id + name + } judge { id created diff --git a/apps/native/app/src/graphql/fragments/vehicle.fragment.graphql b/apps/native/app/src/graphql/fragments/vehicle.fragment.graphql index 244ad841d50a..0ab686f97ab0 100644 --- a/apps/native/app/src/graphql/fragments/vehicle.fragment.graphql +++ b/apps/native/app/src/graphql/fragments/vehicle.fragment.graphql @@ -1,39 +1,10 @@ -fragment VehicleFragment on VehiclesVehicle { - isCurrent +fragment VehicleFragment on VehicleListed { permno regno - vin - type - color - firstRegDate + make + colorName modelYear - productYear - registrationType - role - operatorStartDate - operatorEndDate - outOfUse - otherOwners - termination - buyerPersidno - ownerPersidno - vehicleStatus - useGroup - vehGroup - plateStatus - nextInspection { - nextInspectionDate - nextInspectionDateIfPassedInspectionToday - } - operatorNumber - primaryOperator - ownerSsid - ownerName - lastInspectionResult - lastInspectionDate - lastInspectionType - nextInspectionDate - nextAvailableMileageReadDate requiresMileageRegistration canRegisterMileage + nextMainInspection } diff --git a/apps/native/app/src/graphql/queries/vehicles.graphql b/apps/native/app/src/graphql/queries/vehicles.graphql index da2ad3c69212..a88bb380e3b7 100644 --- a/apps/native/app/src/graphql/queries/vehicles.graphql +++ b/apps/native/app/src/graphql/queries/vehicles.graphql @@ -1,5 +1,5 @@ -query ListVehicles($input: GetVehiclesForUserInput!) { - vehiclesList(input: $input) { +query ListVehiclesV2($input: GetVehiclesListV2Input!) { + vehiclesListV2(input: $input) { vehicleList { ...VehicleFragment } diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx index 057b658de585..f228c0843a88 100644 --- a/apps/native/app/src/screens/home/home.tsx +++ b/apps/native/app/src/screens/home/home.tsx @@ -55,7 +55,7 @@ import { } from './licenses-module' import { OnboardingModule } from './onboarding-module' import { - useListVehiclesQuery, + useListVehiclesV2Query, validateVehiclesInitialData, VehiclesModule, } from './vehicles-module' @@ -174,13 +174,11 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ skip: !airDiscountWidgetEnabled, }) - const vehiclesRes = useListVehiclesQuery({ + const vehiclesRes = useListVehiclesV2Query({ variables: { input: { page: 1, pageSize: 15, - showDeregeristered: false, - showHistory: false, }, }, skip: !vehiclesWidgetEnabled, diff --git a/apps/native/app/src/screens/home/vehicles-module.tsx b/apps/native/app/src/screens/home/vehicles-module.tsx index 02975f50a8e2..34c5c709c86d 100644 --- a/apps/native/app/src/screens/home/vehicles-module.tsx +++ b/apps/native/app/src/screens/home/vehicles-module.tsx @@ -17,8 +17,8 @@ import illustrationSrc from '../../assets/illustrations/le-moving-s4.png' import { navigateTo } from '../../lib/deep-linking' import { VehicleItem } from '../vehicles/components/vehicle-item' import { - ListVehiclesQuery, - useListVehiclesQuery, + ListVehiclesV2Query, + useListVehiclesV2Query, } from '../../graphql/types/schema' import { screenWidth } from '../../utils/dimensions' @@ -30,7 +30,7 @@ const validateVehiclesInitialData = ({ data, loading, }: { - data: ListVehiclesQuery | undefined + data: ListVehiclesV2Query | undefined loading: boolean }) => { if (loading) { @@ -38,7 +38,7 @@ const validateVehiclesInitialData = ({ } // Only show widget initially if there are vehicles that require mileage registration if ( - data?.vehiclesList?.vehicleList?.some( + data?.vehiclesListV2?.vehicleList?.some( (vehicle) => vehicle.requiresMileageRegistration, ) ) { @@ -49,7 +49,7 @@ const validateVehiclesInitialData = ({ } interface VehiclesModuleProps { - data: ListVehiclesQuery | undefined + data: ListVehiclesV2Query | undefined loading: boolean error?: ApolloError | undefined } @@ -59,7 +59,7 @@ const VehiclesModule = React.memo( const theme = useTheme() const intl = useIntl() - const vehicles = data?.vehiclesList?.vehicleList + const vehicles = data?.vehiclesListV2?.vehicleList // Reorder vehicles so vehicles that require mileage registration are shown first const reorderedVehicles = useMemo( @@ -170,4 +170,4 @@ const VehiclesModule = React.memo( }, ) -export { VehiclesModule, validateVehiclesInitialData, useListVehiclesQuery } +export { VehiclesModule, validateVehiclesInitialData, useListVehiclesV2Query } diff --git a/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx b/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx index 100dc377366b..afe6b8c607a5 100644 --- a/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx +++ b/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx @@ -3,7 +3,7 @@ import React from 'react' import { FormattedDate, FormattedMessage } from 'react-intl' import { SafeAreaView, TouchableHighlight, View, ViewStyle } from 'react-native' import styled, { useTheme } from 'styled-components/native' -import { ListVehiclesQuery } from '../../../graphql/types/schema' +import { ListVehiclesV2Query } from '../../../graphql/types/schema' import { navigateTo } from '../../../lib/deep-linking' function differenceInMonths(a: Date, b: Date) { @@ -11,7 +11,7 @@ function differenceInMonths(a: Date, b: Date) { } type VehicleListItem = NonNullable< - NonNullable['vehicleList'] + NonNullable['vehicleList'] >[0] const Cell = styled(TouchableHighlight)` @@ -31,8 +31,8 @@ export const VehicleItem = React.memo( style?: ViewStyle }) => { const theme = useTheme() - const nextInspection = item?.nextInspection?.nextInspectionDate - ? new Date(item?.nextInspection.nextInspectionDate) + const nextInspection = item?.nextMainInspection + ? new Date(item?.nextMainInspection) : null const isInspectionDeadline = @@ -51,14 +51,14 @@ export const VehicleItem = React.memo( onPress={() => { navigateTo(`/vehicle/`, { id: item.permno, - title: item.type, + title: item.make, }) }} > ['vehicleList'] + NonNullable['vehicleList'] >[0] type ListItem = @@ -61,8 +61,6 @@ const Empty = () => ( const input = { page: 1, pageSize: 10, - showDeregeristered: false, - showHistory: false, } export const VehiclesScreen: NavigationFunctionComponent = ({ @@ -77,7 +75,7 @@ export const VehiclesScreen: NavigationFunctionComponent = ({ const scrollY = useRef(new Animated.Value(0)).current const loadingTimeout = useRef>() - const res = useListVehiclesQuery({ + const res = useListVehiclesV2Query({ variables: { input, }, @@ -135,7 +133,9 @@ export const VehiclesScreen: NavigationFunctionComponent = ({ // Extract key of data const keyExtractor = useCallback( (item: ListItem, index: number) => - item.__typename === 'Skeleton' ? String(item.id) : `${item.vin}${index}`, + item.__typename === 'Skeleton' + ? String(item.id) + : `${item.permno}${index}`, [], ) @@ -147,7 +147,7 @@ export const VehiclesScreen: NavigationFunctionComponent = ({ __typename: 'Skeleton', })) } - return res?.data?.vehiclesList?.vehicleList || [] + return res?.data?.vehiclesListV2?.vehicleList || [] }, [res.data, res.loading]) return ( @@ -184,8 +184,8 @@ export const VehiclesScreen: NavigationFunctionComponent = ({ if (res.loading) { return } - const pageNumber = res.data?.vehiclesList?.paging?.pageNumber ?? 1 - const totalPages = res.data?.vehiclesList?.paging?.totalPages ?? 1 + const pageNumber = res.data?.vehiclesListV2?.paging?.pageNumber ?? 1 + const totalPages = res.data?.vehiclesListV2?.paging?.totalPages ?? 1 if (pageNumber >= totalPages) { return } @@ -200,11 +200,11 @@ export const VehiclesScreen: NavigationFunctionComponent = ({ }, updateQuery(prev, { fetchMoreResult }) { return { - vehiclesList: { - ...fetchMoreResult.vehiclesList, + vehiclesListV2: { + ...fetchMoreResult.vehiclesListV2, vehicleList: [ - ...(prev.vehiclesList?.vehicleList ?? []), - ...(fetchMoreResult.vehiclesList?.vehicleList ?? []), + ...(prev.vehiclesListV2?.vehicleList ?? []), + ...(fetchMoreResult.vehiclesListV2?.vehicleList ?? []), ], }, } diff --git a/apps/services/auth/admin-api/project.json b/apps/services/auth/admin-api/project.json index 2866faa0019a..e5d91fd92127 100644 --- a/apps/services/auth/admin-api/project.json +++ b/apps/services/auth/admin-api/project.json @@ -57,6 +57,17 @@ }, "docker-express": { "executor": "Intentionally left blank, only so this target is valid when using `nx show projects --with-target docker-express`" + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn start --project services-auth-admin-api" + } + ], + "parallel": true + } } } } diff --git a/apps/services/auth/delegation-api/project.json b/apps/services/auth/delegation-api/project.json index 366532998b91..66890516fc3c 100644 --- a/apps/services/auth/delegation-api/project.json +++ b/apps/services/auth/delegation-api/project.json @@ -28,6 +28,17 @@ "buildTarget": "services-auth-delegation-api:build" } }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn start services-auth-delegation-api" + } + ], + "parallel": true + } + }, "lint": { "executor": "@nx/eslint:lint" }, diff --git a/apps/services/auth/ids-api/project.json b/apps/services/auth/ids-api/project.json index cfefdd77acda..b64e70508680 100644 --- a/apps/services/auth/ids-api/project.json +++ b/apps/services/auth/ids-api/project.json @@ -175,6 +175,17 @@ ], "parallel": true } + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn start services-auth-ids-api" + } + ], + "parallel": true + } } } } diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts index f2dee16513d6..faee06788837 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts @@ -1,7 +1,7 @@ import { getModelToken } from '@nestjs/sequelize' -import addDays from 'date-fns/addDays' import request from 'supertest' import { uuid } from 'uuidv4' +import addDays from 'date-fns/addDays' import { ApiScope, @@ -27,12 +27,16 @@ import { } from '@island.is/shared/types' import { createCurrentUser, + createNationalId, createNationalRegistryUser, } from '@island.is/testing/fixtures' import { TestApp } from '@island.is/testing/nest' import { defaultScopes, setupWithAuth } from '../../../../test/setup' -import { getFakeNationalId } from '../../../../test/stubs/genericStubs' +import { + getFakeCompanyNationalId, + getFakeNationalId, +} from '../../../../test/stubs/genericStubs' describe('DelegationsController', () => { describe.each([false, true])( @@ -57,6 +61,8 @@ describe('DelegationsController', () => { clientId: '@island.is/webapp', }) + const representeeNationalId = createNationalId('person') + const scopeValid1 = 'scope/valid1' const scopeValid2 = 'scope/valid2' const scopeValid1and2 = 'scope/valid1and2' @@ -76,7 +82,7 @@ describe('DelegationsController', () => { scopeName: s, })) - const userNationalId = getFakeNationalId() + const userNationalId = createNationalId('person') const user = createCurrentUser({ nationalId: userNationalId, @@ -85,6 +91,8 @@ describe('DelegationsController', () => { }) const domain = createDomain() + let nationalRegistryApiSpy: jest.SpyInstance + let nationalRegistryV3ApiSpy: jest.SpyInstance beforeAll(async () => { app = await setupWithAuth({ @@ -117,36 +125,214 @@ describe('DelegationsController', () => { >(getModelToken(DelegationDelegationType)) nationalRegistryApi = app.get(NationalRegistryClientService) nationalRegistryV3Api = app.get(NationalRegistryV3ClientService) + factory = new FixtureFactory(app) + + client.supportedDelegationTypes = [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + ] + await factory.createClient(client) + + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return user ?? null + }) + + nationalRegistryV3ApiSpy = jest + .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') + .mockImplementation(async () => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return { kennitala: user.nationalId, nafn: user.name } + }) + const nationalRegistryV3FeatureService = app.get( NationalRegistryV3FeatureService, ) jest .spyOn(nationalRegistryV3FeatureService, 'getValue') .mockImplementation(async () => featureFlag) - factory = new FixtureFactory(app) }) afterAll(async () => { await app.cleanUp() + nationalRegistryV3ApiSpy.mockClear() + nationalRegistryApiSpy.mockClear() }) - describe('GET with general mandate delegation type', () => { - const representeeNationalId = getFakeNationalId() - let nationalRegistryApiSpy: jest.SpyInstance - let nationalRegistryV3ApiSpy: jest.SpyInstance + describe('GET with general mandate delegation type for company', () => { + const companyNationalId = getFakeCompanyNationalId() + const scopeNames = [ 'api-scope/generalMandate1', 'api-scope/generalMandate2', 'api-scope/generalMandate3', + 'api-scope/procuration1', + 'api-scope/procuration2', ] beforeAll(async () => { - client.supportedDelegationTypes = [ - AuthDelegationType.GeneralMandate, - AuthDelegationType.LegalGuardian, + const delegations = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Company', + fromNationalId: companyNationalId, + toNationalId: userNationalId, + toName: 'Person', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegations.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + await apiScopeModel.bulkCreate( + scopeNames.map((name) => ({ + name, + domainName: domain.name, + enabled: true, + description: `${name}: description`, + displayName: `${name}: display name`, + })), + ) + + await apiScopeDelegationTypeModel.bulkCreate([ + { + apiScopeName: scopeNames[0], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[1], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[3], + delegationType: AuthDelegationType.ProcurationHolder, + }, + { + apiScopeName: scopeNames[4], + delegationType: AuthDelegationType.ProcurationHolder, + }, + ]) + }) + + afterAll(async () => { + await apiScopeDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await apiScopeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + }) + + it('should return mergedDelegationDTO with the generalMandate', async () => { + const response = await server.get('/v2/delegations') + + expect(response.status).toEqual(200) + expect(response.body).toHaveLength(1) + }) + + it('should return all general mandate scopes and other preset scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: companyNationalId, + delegationType: [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.ProcurationHolder, + ], + }) + + const expected = [ + scopeNames[0], + scopeNames[1], + scopeNames[3], + scopeNames[4], + ] + + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(expected)) + expect(response.body).toHaveLength(expected.length) + }) + + it('should return all general mandate scopes and all procuration scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: companyNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + const expected = [ + scopeNames[0], + scopeNames[1], + scopeNames[3], + scopeNames[4], ] - await factory.createClient(client) + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(expected)) + expect(response.body).toHaveLength(expected.length) + }) + + it('should return all general mandate scopes, and not procuration scopes since from nationalId is person', async () => { + // Assert + const delegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'FromPersonPerson', + fromNationalId: representeeNationalId, + toNationalId: userNationalId, + toName: 'Person', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + // Act + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + const expected = [scopeNames[0], scopeNames[1]] + + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(expected)) + expect(response.body).toHaveLength(expected.length) + }) + }) + + describe('GET with general mandate delegation type', () => { + const scopeNames = [ + 'api-scope/generalMandate1', + 'api-scope/generalMandate2', + 'api-scope/generalMandate3', + ] + + beforeAll(async () => { const delegations = await delegationModel.create({ id: uuid(), fromDisplayName: 'Test', @@ -187,32 +373,39 @@ describe('DelegationsController', () => { delegationType: AuthDelegationType.GeneralMandate, }, ]) - - nationalRegistryApiSpy = jest - .spyOn(nationalRegistryApi, 'getIndividual') - .mockImplementation(async (id) => { - const user = createNationalRegistryUser({ - nationalId: representeeNationalId, - }) - - return user ?? null - }) - - nationalRegistryV3ApiSpy = jest - .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') - .mockImplementation(async () => { - const user = createNationalRegistryUser({ - nationalId: representeeNationalId, - }) - - return { kennitala: user.nationalId, nafn: user.name } - }) }) afterAll(async () => { - await app.cleanUp() - nationalRegistryApiSpy.mockClear() - nationalRegistryV3ApiSpy.mockClear() + await apiScopeDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await apiScopeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationProviderModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) }) it('should return mergedDelegationDTO with the generalMandate', async () => { diff --git a/apps/services/auth/ids-api/test/stubs/genericStubs.ts b/apps/services/auth/ids-api/test/stubs/genericStubs.ts index 91956b00fcc5..177ad31ae023 100644 --- a/apps/services/auth/ids-api/test/stubs/genericStubs.ts +++ b/apps/services/auth/ids-api/test/stubs/genericStubs.ts @@ -6,6 +6,8 @@ export type NameIdTuple = [name: string, id: string] export const getFakeNationalId = () => faker.helpers.replaceSymbolWithNumber('##########') +export const getFakeCompanyNationalId = () => createNationalId('company') + export const getFakeName = () => faker.fake('{{name.firstName}} {{name.lastName}}') @@ -18,4 +20,5 @@ export default { getFakeNationalId, getFakeName, getFakePerson, + getFakeCompanyNationalId, } diff --git a/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts b/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts index da5d5c7e63a9..26deb3a189a6 100644 --- a/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts +++ b/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts @@ -52,7 +52,7 @@ export class MeNotificationsController { @CurrentUser() user: User, @Query() query: ExtendedPaginationDto, ): Promise { - return this.notificationService.findMany(user, query) + return this.notificationService.findManyWithTemplate(user.nationalId, query) } @Get('/unread-count') diff --git a/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts b/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts index a1425841a8b8..c8f578ce6397 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts @@ -3,10 +3,13 @@ import { Body, Controller, Get, + Headers, + HttpStatus, Inject, Param, Post, Query, + UseGuards, Version, } from '@nestjs/common' import { ApiTags } from '@nestjs/swagger' @@ -19,6 +22,12 @@ import { CreateHnippNotificationDto } from './dto/createHnippNotification.dto' import { HnippTemplate } from './dto/hnippTemplate.response' import { NotificationsService } from './notifications.service' import type { Locale } from '@island.is/shared/types' +import { + ExtendedPaginationDto, + PaginatedNotificationDto, +} from './dto/notification.dto' +import { IdsUserGuard, Scopes, ScopesGuard } from '@island.is/auth-nest-tools' +import { AdminPortalScope } from '@island.is/auth/scopes' @Controller('notifications') @ApiTags('notifications') @@ -85,6 +94,20 @@ export class NotificationsController { return await this.notificationsService.getTemplate(templateId, locale) } + @UseGuards(IdsUserGuard, ScopesGuard) + @Scopes(AdminPortalScope.serviceDesk) + @Get('/') + @Documentation({ + summary: 'Returns a paginated list of notifications for a national id', + response: { status: HttpStatus.OK, type: PaginatedNotificationDto }, + }) + findMany( + @Headers('X-Query-National-Id') nationalId: string, + @Query() query: ExtendedPaginationDto, + ): Promise { + return this.notificationsService.findMany(nationalId, query) + } + @Documentation({ summary: 'Creates a new notification and adds to queue', includeNoContentResponse: true, diff --git a/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts b/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts index 8d8e937ff0f7..4116120d2744 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts @@ -260,8 +260,24 @@ export class NotificationsService { ) } - async findMany( - user: User, + findMany( + nationalId: string, + query: ExtendedPaginationDto, + ): Promise { + return paginate({ + Model: this.notificationModel, + limit: query.limit || 10, + after: query.after || '', + before: query.before, + primaryKeyField: 'id', + orderOption: [['id', 'DESC']], + where: { recipient: nationalId }, + attributes: ['id', 'messageId', 'senderId', 'created', 'updated'], + }) + } + + async findManyWithTemplate( + nationalId: string, query: ExtendedPaginationDto, ): Promise { const locale = mapToLocale(query.locale as Locale) @@ -273,7 +289,7 @@ export class NotificationsService { before: query.before, primaryKeyField: 'id', orderOption: [['id', 'DESC']], - where: { recipient: user.nationalId }, + where: { recipient: nationalId }, }) const formattedNotifications = await Promise.all( diff --git a/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts b/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts index 8820d8ddd2cb..8456f9a5333e 100644 --- a/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts +++ b/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts @@ -159,7 +159,9 @@ describe('NotificationsService', () => { .spyOn(service, 'findMany') .mockImplementation(async () => mockedResponse) - expect(await service.findMany(user, query)).toBe(mockedResponse) + expect(await service.findMany(user.nationalId, query)).toBe( + mockedResponse, + ) }) }) diff --git a/apps/web/components/connected/WHODAS/Calculator.tsx b/apps/web/components/connected/WHODAS/Calculator.tsx index 109eb573ccfa..53d09cfad3cb 100644 --- a/apps/web/components/connected/WHODAS/Calculator.tsx +++ b/apps/web/components/connected/WHODAS/Calculator.tsx @@ -191,36 +191,42 @@ const WHODASResults = ({ - - - {formatMessage(m.results.breakdownHeading)} - - - {results.steps.map((step) => ( - - - {step.title} - {formatScore(step.scoreForStep)} + {bracket > 1 && ( + + + + + {formatMessage(m.results.breakdownHeading)} + + + {results.steps.map((step) => ( + + + {step.title} + {formatScore(step.scoreForStep)} + + + ))} + + + + {formatMessage(m.results.totalScore)} + + {formatScore(totalScore)} - - ))} - - - - {formatMessage(m.results.totalScore)} - - {formatScore(totalScore)} - - + + + + {formatMessage(m.results.resultDisclaimer)} + + + )} - - {formatMessage(m.results.resultDisclaimer)} - ) } @@ -246,9 +252,10 @@ export const WHODASCalculator = ({ slice }: WHODASCalculatorProps) => { description, maxScorePossible: questions.reduce( (prev, acc) => - prev + acc.answerOptions.length > 0 + prev + + (acc.answerOptions.length > 0 ? acc.answerOptions[acc.answerOptions.length - 1].score - : 0, + : 0), 0, ), questions: questions.map(() => ({ diff --git a/libs/api/domains/notifications/src/lib/notifications.model.ts b/libs/api/domains/notifications/src/lib/notifications.model.ts index 64436d89583a..2c83f4a8107c 100644 --- a/libs/api/domains/notifications/src/lib/notifications.model.ts +++ b/libs/api/domains/notifications/src/lib/notifications.model.ts @@ -89,6 +89,21 @@ export class Notification { message!: NotificationMessage } +@ObjectType() +export class AdminNotification { + @Field(() => Int) + id!: number + + @Field(() => ID) + notificationId!: string + + @Field(() => NotificationSender) + sender!: NotificationSender + + @Field(() => GraphQLISODateTime) + sent!: Date +} + @InputType() export class NotificationsInput extends PaginationInput() {} @@ -101,6 +116,11 @@ export class NotificationsResponse extends PaginatedResponse(Notification) { unseenCount?: number } +@ObjectType('AdminNotifications') +export class AdminNotificationsResponse extends PaginatedResponse( + AdminNotification, +) {} + @ObjectType() export class NotificationResponse { @Field(() => Notification) diff --git a/libs/api/domains/notifications/src/lib/notifications.module.ts b/libs/api/domains/notifications/src/lib/notifications.module.ts index 745702eb28bb..231c0d96c879 100644 --- a/libs/api/domains/notifications/src/lib/notifications.module.ts +++ b/libs/api/domains/notifications/src/lib/notifications.module.ts @@ -8,6 +8,8 @@ import { NotificationSenderResolver, } from './notificationsList.resolver' import { NotificationsService } from './notifications.service' +import { NotificationsAdminResolver } from './notificationsAdmin.resolver' +import { NotificationsAdminService } from './notificationsAdmin.service' @Module({ imports: [UserNotificationClientModule], @@ -15,7 +17,9 @@ import { NotificationsService } from './notifications.service' NotificationsResolver, NotificationsListResolver, NotificationSenderResolver, + NotificationsAdminResolver, NotificationsService, + NotificationsAdminService, ], exports: [], }) diff --git a/libs/api/domains/notifications/src/lib/notificationsAdmin.resolver.ts b/libs/api/domains/notifications/src/lib/notificationsAdmin.resolver.ts new file mode 100644 index 000000000000..9afd668e3173 --- /dev/null +++ b/libs/api/domains/notifications/src/lib/notificationsAdmin.resolver.ts @@ -0,0 +1,60 @@ +import { Args, Query, Resolver } from '@nestjs/graphql' +import { IdsUserGuard, CurrentUser } from '@island.is/auth-nest-tools' +import type { User } from '@island.is/auth-nest-tools' +import { Audit } from '@island.is/nest/audit' +import { Inject, UseGuards } from '@nestjs/common' + +import { NotificationsAdminService } from './notificationsAdmin.service' +import { + NotificationsInput, + AdminNotificationsResponse, +} from './notifications.model' +import type { Locale } from '@island.is/shared/types' +import { LOGGER_PROVIDER, type Logger } from '@island.is/logging' + +const LOG_CATEGORY = 'notification-admin-resolver' +export const AUDIT_NAMESPACE = 'notifications-admin-resolver' + +@UseGuards(IdsUserGuard) +@Resolver(() => AdminNotificationsResponse) +@Audit({ namespace: AUDIT_NAMESPACE }) +export class NotificationsAdminResolver { + constructor( + private readonly service: NotificationsAdminService, + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + ) {} + + @Query(() => AdminNotificationsResponse, { + name: 'adminNotifications', + nullable: true, + }) + @Audit() + async getNotifications( + @Args('nationalId') nationalId: string, + @Args('input', { type: () => NotificationsInput, nullable: true }) + input: NotificationsInput, + @CurrentUser() user: User, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + ): Promise { + let notifications: AdminNotificationsResponse | null + + try { + notifications = await this.service.getNotifications( + locale, + nationalId, + user, + input, + ) + } catch (e) { + this.logger.error('failed to get admin notifications', { + locale, + category: LOG_CATEGORY, + error: e, + }) + throw e + } + + return notifications + } +} diff --git a/libs/api/domains/notifications/src/lib/notificationsAdmin.service.ts b/libs/api/domains/notifications/src/lib/notificationsAdmin.service.ts new file mode 100644 index 000000000000..76089eff1156 --- /dev/null +++ b/libs/api/domains/notifications/src/lib/notificationsAdmin.service.ts @@ -0,0 +1,52 @@ +import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' +import { LOGGER_PROVIDER } from '@island.is/logging' +import type { Logger } from '@island.is/logging' +import { Inject, Injectable } from '@nestjs/common' +import { NotificationsApi } from '@island.is/clients/user-notification' +import type { Locale } from '@island.is/shared/types' +import { + AdminNotificationsResponse, + NotificationsInput, +} from './notifications.model' +import { adminNotificationMapper } from '../utils/helpers' + +@Injectable() +export class NotificationsAdminService { + constructor( + @Inject(LOGGER_PROVIDER) + private logger: Logger, + private notificationsApi: NotificationsApi, + ) {} + + notificationsWAuth(auth: Auth) { + return this.notificationsApi.withMiddleware(new AuthMiddleware(auth)) + } + + async getNotifications( + locale: Locale, + nationalId: string, + user: User, + input?: NotificationsInput, + ): Promise { + const notifications = await this.notificationsWAuth( + user, + ).notificationsControllerFindMany({ + xQueryNationalId: nationalId, + locale, + limit: input?.limit, + before: input?.before, + after: input?.after, + }) + + if (!notifications.data) { + this.logger.debug('no admin notification found') + return null + } + + return { + data: notifications.data.map((item) => adminNotificationMapper(item)), + totalCount: notifications.totalCount, + pageInfo: notifications.pageInfo, + } + } +} diff --git a/libs/api/domains/notifications/src/utils/helpers.ts b/libs/api/domains/notifications/src/utils/helpers.ts index 3baac1e1b7fc..87932276f1e9 100644 --- a/libs/api/domains/notifications/src/utils/helpers.ts +++ b/libs/api/domains/notifications/src/utils/helpers.ts @@ -1,5 +1,5 @@ import { RenderedNotificationDto } from '@island.is/clients/user-notification' -import { Notification } from '../lib/notifications.model' +import { AdminNotification, Notification } from '../lib/notifications.model' const cleanString = (str?: string) => { if (!str) { @@ -40,3 +40,13 @@ export const notificationMapper = ( }, }, }) +export const adminNotificationMapper = ( + notification: RenderedNotificationDto, +): AdminNotification => ({ + id: notification.id, + notificationId: notification.messageId, + sent: notification.created, + sender: { + id: notification.senderId, + }, +}) diff --git a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx index 44bee990cc2e..ce66ffa0bc15 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx @@ -1,6 +1,5 @@ -import { SkeletonLoader } from '@island.is/island-ui/core' +import { Input, SkeletonLoader } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' -import { InputController } from '@island.is/shared/form-fields' import { MessageDescriptor } from 'react-intl' import { OJOI_INPUT_HEIGHT } from '../../lib/constants' import { useApplication } from '../../hooks/useUpdateApplication' @@ -60,7 +59,7 @@ export const OJOIInputController = ({ } return ( - = { label: string - value: string + value: T } -type Props = { +type Props = { name: string label: string | MessageDescriptor placeholder: string | MessageDescriptor - options?: OJOISelectControllerOption[] - defaultValue?: string + options?: SelectOption[] + defaultValue?: T loading?: boolean applicationId: string disabled?: boolean - onChange?: (label: string, value: string) => void + onBeforeChange?: (answers: OJOIApplication['answers'], value: T) => void + onChange?: (value: T) => void } -export const OJOISelectController = ({ +export const OJOISelectController = ({ name, label, placeholder, @@ -33,30 +36,41 @@ export const OJOISelectController = ({ loading, applicationId, disabled, + onBeforeChange, onChange, -}: Props) => { +}: Props) => { const { formatMessage: f } = useLocale() - const { updateApplication, application } = useApplication({ applicationId }) + const { updateApplication, application } = useApplication({ + applicationId, + }) + + const { setValue } = useFormContext() const placeholderText = typeof placeholder === 'string' ? placeholder : f(placeholder) const labelText = typeof label === 'string' ? label : f(label) - const handleChange = (label: string, value: string) => { + const handleChange = (value: T) => { const currentAnswers = structuredClone(application.answers) const newAnswers = set(currentAnswers, name, value) + onBeforeChange && onBeforeChange(newAnswers, value) - // we must reset the selected typeId if the department changes - if (name === InputFields.advert.departmentId) { - set(newAnswers, InputFields.advert.typeId, '') - } - + setValue(name, value) updateApplication(newAnswers) - onChange && onChange(label, value) + onChange && onChange(value) } + const defaultVal = getValueViaPath(application.answers, name, defaultValue) + const defaultOpt = options?.find((opt) => { + if (isBaseEntity(opt.value) && isBaseEntity(defaultVal)) { + return opt.value.id === defaultVal.id + } + + return false + }) + if (loading) { return ( handleChange(opt.label, opt.value)} + defaultValue={defaultOpt} + onChange={(opt) => { + if (!opt?.value) return + return handleChange(opt.value) + }} /> ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Advert.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Advert.tsx index fe9cc36440ae..9cb1b1371782 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Advert.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Advert.tsx @@ -1,4 +1,3 @@ -import { useCallback } from 'react' import { InputFields, OJOIFieldBaseProps } from '../lib/types' import { Box } from '@island.is/island-ui/core' import { FormGroup } from '../components/form/FormGroup' @@ -15,86 +14,80 @@ import set from 'lodash/set' import { HTMLEditor } from '../components/htmlEditor/HTMLEditor' import { getAdvertMarkup } from '../lib/utils' -type Props = OJOIFieldBaseProps & { - timeStamp: string -} - -export const Advert = ({ application, timeStamp }: Props) => { +export const Advert = ({ application }: OJOIFieldBaseProps) => { const { setValue } = useFormContext() - const { application: currentApplication, updateApplication } = useApplication( - { - applicationId: application.id, - }, - ) + const { application: currentApplication } = useApplication({ + applicationId: application.id, + }) const { departments, loading: loadingDepartments } = useDepartments() const { - useLazyTypes, + getLazyTypes, types, loading: loadingTypes, } = useTypes({ - initalDepartmentId: application.answers?.advert?.departmentId, + initalDepartmentId: application.answers?.advert?.department?.id, }) - const handleDepartmentChange = useCallback( - (value: string) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - useLazyTypes({ - params: { - department: value, - pageSize: 100, - }, - }) - }, - [useLazyTypes], - ) - - const updateTypeHandler = (name: string, id: string) => { - let currentAnswers = structuredClone(currentApplication.answers) - currentAnswers = set(currentAnswers, InputFields.advert.typeName, name) - - currentAnswers = set(currentAnswers, InputFields.advert.typeId, id) - - updateApplication(currentAnswers) - } - const titlePreview = getAdvertMarkup({ - type: currentApplication.answers.advert?.typeName, + type: currentApplication.answers.advert?.type?.title, title: currentApplication.answers.advert?.title, }) + const departmentOptions = departments?.map((d) => ({ + label: d.title, + value: { + id: d.id, + title: d.title, + slug: d.slug, + }, + })) + + const typeOptions = types?.map((d) => ({ + label: d.title, + value: { + id: d.id, + title: d.title, + slug: d.slug, + }, + })) + return ( <> ({ - label: d.title, - value: d.id, - }))} - onChange={(_, value) => handleDepartmentChange(value)} + options={departmentOptions} + defaultValue={application.answers?.advert?.department} + onBeforeChange={(answers) => { + setValue(InputFields.advert.type, null) + set(answers, InputFields.advert.type, null) + }} + onChange={(value) => + getLazyTypes({ + variables: { + params: { + department: value.id, + pageSize: 100, + }, + }, + }) + } /> ({ - label: d.title, - value: d.id, - }))} - onChange={(label, value) => { - updateTypeHandler(label, value) - }} + options={typeOptions} /> @@ -132,7 +125,6 @@ export const Advert = ({ application, timeStamp }: Props) => { applicationId={application.id} name={InputFields.advert.html} defaultValue={currentApplication.answers?.advert?.html} - editorKey={timeStamp} // we have use setValue from useFormContext to update the value // because this is not a controlled component onChange={(value) => setValue(InputFields.advert.html, value)} diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/AdvertModal.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/AdvertModal.tsx index ba50ca2999dc..8327cefa5e93 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/AdvertModal.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/AdvertModal.tsx @@ -1,5 +1,4 @@ import { - AlertMessage, Box, Button, Icon, @@ -23,11 +22,10 @@ import { DEFAULT_PAGE_SIZE, OJOI_INPUT_HEIGHT, } from '../lib/constants' -import { useAdvert } from '../hooks/useAdvert' import debounce from 'lodash/debounce' -import set from 'lodash/set' import { InputFields } from '../lib/types' import { useFormContext } from 'react-hook-form' +import { OfficialJournalOfIcelandAdvert } from '@island.is/api/schema' type Props = { applicationId: string visible: boolean @@ -35,55 +33,30 @@ type Props = { onConfirmChange?: () => void } -type UpdateAdvertFields = { - title: string - departmentId: string - typeId: string - html: string - categories: string[] -} - export const AdvertModal = ({ applicationId, visible, setVisible, onConfirmChange, }: Props) => { - const [page, setPage] = useState(DEFAULT_PAGE) - const [search, setSearch] = useState('') - const [selectedAdvertId, setSelectedAdvertId] = useState(null) - const { formatMessage: f } = useLocale() const { setValue } = useFormContext() const { application, updateApplication } = useApplication({ applicationId, }) + const [page, setPage] = useState(DEFAULT_PAGE) + const [search, setSearch] = useState('') + const [selectedAdvert, setSelectedAdvert] = + useState(null) + const { adverts, paging, loading } = useAdverts({ page: page, search: search, }) - const [updateAdvertFields, setUpdateAdvertFields] = - useState(null) - - const { loading: loadingAdvert, error: advertError } = useAdvert({ - advertId: selectedAdvertId, - onCompleted: (ad) => { - setUpdateAdvertFields({ - title: ad.title, - departmentId: ad.department.id, - typeId: ad.type.id, - html: ad.document.html, - categories: ad.categories.map((c) => c.id), - }) - }, - }) - - const disableConfirmButton = !selectedAdvertId || !!advertError - - const onSelectAdvert = (advertId: string) => { - setSelectedAdvertId(advertId) + const onSelectAdvert = (advert: OfficialJournalOfIcelandAdvert) => { + setSelectedAdvert(advert) } const onSearchChange = (value: string) => { @@ -97,46 +70,41 @@ export const AdvertModal = ({ debouncedSearch(e.target.value) } - const onConfirm = () => { - if (!updateAdvertFields) { + const onConfirm = (advert: OfficialJournalOfIcelandAdvert | null) => { + if (!advert) { return } - const currentAnswers = structuredClone(application.answers) + const clean = (obj: { + __typename?: string + id: string + title: string + slug: string + }) => { + const { __typename: _, ...rest } = obj + return rest + } + + const department = clean(advert.department) + const type = clean(advert.type) - let updatedAnswers = set( - currentAnswers, - InputFields.advert.title, - updateAdvertFields.title, - ) - updatedAnswers = set( - updatedAnswers, - InputFields.advert.departmentId, - updateAdvertFields.departmentId, - ) - updatedAnswers = set( - updatedAnswers, - InputFields.advert.typeId, - updateAdvertFields.typeId, - ) - updatedAnswers = set( - updatedAnswers, - InputFields.advert.html, - updateAdvertFields.html, - ) - updatedAnswers = set( - updatedAnswers, - InputFields.advert.categories, - updateAdvertFields.categories, - ) + const categories = advert.categories.map((category) => clean(category)) - setValue(InputFields.advert.title, updateAdvertFields.title) - setValue(InputFields.advert.departmentId, updateAdvertFields.departmentId) - setValue(InputFields.advert.typeId, updateAdvertFields.typeId) - setValue(InputFields.advert.html, updateAdvertFields.html) - setValue(InputFields.advert.categories, updateAdvertFields.categories) + setValue(InputFields.advert.department, department) + setValue(InputFields.advert.type, type) + setValue(InputFields.advert.title, advert.title) + + updateApplication({ + ...application.answers, + advert: { + department, + type, + categories, + title: advert.title, + html: advert.document.html, + }, + }) - updateApplication(updatedAnswers) onConfirmChange && onConfirmChange() setVisible(false) } @@ -173,13 +141,6 @@ export const AdvertModal = ({ onChange={handleSearchChange} /> - {!!advertError && ( - - )} onSelectAdvert(advert.id)} + checked={selectedAdvert?.id === advert.id} + onChange={() => onSelectAdvert(advert)} /> ))} @@ -233,9 +194,8 @@ export const AdvertModal = ({ {f(general.cancel)} diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx index 3255b2e091e5..82257eeee648 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx @@ -21,7 +21,6 @@ import { import { Routes, SignatureTypes } from '../lib/constants' import { useApplication } from '../hooks/useUpdateApplication' import { advert, error, preview, signatures } from '../lib/messages' -import { useType } from '../hooks/useType' import { previewValidationSchema, signatureValidationSchema, @@ -39,10 +38,6 @@ export const Preview = ({ application, goToScreen }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() - const { type } = useType({ - typeId: currentApplication.answers.advert?.typeId, - }) - const { fetchPdf, error: pdfError, @@ -60,7 +55,7 @@ export const Preview = ({ application, goToScreen }: OJOIFieldBaseProps) => { const url = URL.createObjectURL(blob) let downloadName - const type = currentApplication.answers.advert?.typeName + const type = currentApplication.answers.advert?.type?.title if (type) { downloadName = type.replace('.', '') } @@ -103,14 +98,14 @@ export const Preview = ({ application, goToScreen }: OJOIFieldBaseProps) => { }) const advertMarkup = getAdvertMarkup({ - type: type?.title, + type: currentApplication.answers.advert?.type?.title, title: currentApplication.answers.advert?.title, html: currentApplication.answers.advert?.html, }) const hasMarkup = !!currentApplication.answers.advert?.html || - type?.title || + currentApplication.answers.advert?.type?.title || currentApplication.answers.advert?.title const combinedHtml = hasMarkup diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx index b2f7d27726a9..2b2b845b4092 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx @@ -8,6 +8,7 @@ import { AlertMessage, Box, Icon, + Inline, Select, SkeletonLoader, Tag, @@ -18,6 +19,8 @@ import set from 'lodash/set' import addYears from 'date-fns/addYears' import { addWeekdays, getFastTrack, getWeekendDates } from '../lib/utils' import { useState } from 'react' +import { baseEntitySchema } from '../lib/dataSchema' +import { z } from 'zod' export const Publishing = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() @@ -52,7 +55,7 @@ export const Publishing = ({ application }: OJOIFieldBaseProps) => { getFastTrack(new Date(defaultDate)).fastTrack, ) - const onCategoryChange = (value?: string) => { + const onCategoryChange = (value?: z.infer) => { setIsUpdatingCategory(true) if (!value) { setIsUpdatingCategory(false) @@ -62,8 +65,8 @@ export const Publishing = ({ application }: OJOIFieldBaseProps) => { const currentAnswers = structuredClone(currentApplication.answers) const selectedCategories = currentAnswers.advert?.categories || [] - const newCategories = selectedCategories.includes(value) - ? selectedCategories.filter((c) => c !== value) + const newCategories = selectedCategories.find((cat) => cat.id === value.id) + ? selectedCategories.filter((c) => c.id !== value.id) : [...selectedCategories, value] const updatedAnswers = set( @@ -77,19 +80,12 @@ export const Publishing = ({ application }: OJOIFieldBaseProps) => { }) } - const defaultCategory = { - label: f(publishing.inputs.contentCategories.placeholder), - value: '', - } - const mappedCategories = categories?.map((c) => ({ label: c.title, - value: c.id, + value: c, })) - const selectedCategories = categories?.filter((c) => - currentApplication.answers.advert?.categories?.includes(c.id), - ) + const selectedCategories = currentApplication.answers.advert?.categories return ( @@ -124,30 +120,26 @@ export const Publishing = ({ application }: OJOIFieldBaseProps) => { size="sm" label={f(publishing.inputs.contentCategories.label)} backgroundColor="blue" - defaultValue={defaultCategory} options={mappedCategories} + defaultValue={mappedCategories?.[0]} onChange={(opt) => onCategoryChange(opt?.value)} /> - - {selectedCategories?.map((c) => ( - onCategoryChange(c.id)} - outlined - key={c.id} - > - - {c.title} - - - - ))} + + + {selectedCategories?.map((c) => ( + onCategoryChange(c)} + outlined + key={c.id} + > + + {c.title} + + + + ))} + )} diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx index 946abdbf7d5b..dc1799e2a3ef 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx @@ -14,12 +14,9 @@ import { useUserInfo } from '@island.is/react-spa/bff' import { useEffect } from 'react' import { ZodCustomIssue } from 'zod' import { Property } from '../components/property/Property' -import { useCategories } from '../hooks/useCategories' -import { useDepartment } from '../hooks/useDepartment' import { usePrice } from '../hooks/usePrice' -import { useType } from '../hooks/useType' import { useApplication } from '../hooks/useUpdateApplication' -import { MINIMUM_WEEKDAYS, Routes } from '../lib/constants' +import { Routes } from '../lib/constants' import { advertValidationSchema, publishingValidationSchema, @@ -28,7 +25,7 @@ import { import { advert, error, publishing, summary } from '../lib/messages' import { signatures } from '../lib/messages/signatures' import { OJOIFieldBaseProps } from '../lib/types' -import { addWeekdays, getFastTrack, parseZodIssue } from '../lib/utils' +import { getFastTrack, parseZodIssue } from '../lib/utils' export const Summary = ({ application, @@ -42,26 +39,11 @@ export const Summary = ({ const user = useUserInfo() - const { type, loading: loadingType } = useType({ - typeId: currentApplication.answers.advert?.typeId, - }) - const { price, loading: loadingPrice } = usePrice({ applicationId: application.id, }) - const { department, loading: loadingDepartment } = useDepartment({ - departmentId: currentApplication.answers.advert?.departmentId, - }) - - const { categories, loading: loadingCategories } = useCategories() - - const selectedCategories = categories?.filter((c) => - currentApplication.answers?.advert?.categories?.includes(c.id), - ) - - const today = new Date() - const estimatedDate = addWeekdays(today, MINIMUM_WEEKDAYS) + const selectedCategories = application.answers?.advert?.categories const advertValidationCheck = advertValidationSchema.safeParse( currentApplication.answers, @@ -86,6 +68,10 @@ export const Summary = ({ } else { setSubmitButtonDisabled && setSubmitButtonDisabled(true) } + + return () => { + setSubmitButtonDisabled && setSubmitButtonDisabled(false) + } }, [ advertValidationCheck, signatureValidationCheck, @@ -227,18 +213,16 @@ export const Summary = ({ value={user.profile.name} /> c.title).join(', ')} /> @@ -289,7 +272,6 @@ export const Summary = ({ } /> diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts index ca15965c5c74..8cd4ff5678ed 100644 --- a/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts @@ -1,4 +1,4 @@ -import { NetworkStatus, useQuery } from '@apollo/client' +import { useLazyQuery, useQuery } from '@apollo/client' import { OfficialJournalOfIcelandAdvertsTypesResponse } from '@island.is/api/schema' import { TYPES_QUERY } from '../graphql/queries' @@ -40,21 +40,35 @@ export const useTypes = ({ params.pageSize = 1000 } - const { data, loading, error, refetch, networkStatus } = useQuery< - TypesResponse, - TypesVariables - >(TYPES_QUERY, { - variables: { - params: params, + const { data, loading, error } = useQuery( + TYPES_QUERY, + { + variables: { + params: params, + }, + onCompleted: onCompleted, }, - notifyOnNetworkStatusChange: true, - onCompleted: onCompleted, + ) + + const [ + getLazyTypes, + { data: lazyTypes, loading: lazyTypesLoading, error: lazyTypesError }, + ] = useLazyQuery(TYPES_QUERY, { + fetchPolicy: 'network-only', }) + const currentTypes = lazyTypes + ? lazyTypes.officialJournalOfIcelandTypes.types + : data?.officialJournalOfIcelandTypes.types + return { - useLazyTypes: refetch, - types: data?.officialJournalOfIcelandTypes.types, - loading: loading || networkStatus === NetworkStatus.refetch, + lazyTypes: lazyTypes?.officialJournalOfIcelandTypes.types, + lazyTypesLoading, + lazyTypesError, + getLazyTypes, + types: currentTypes, + initalTypes: data?.officialJournalOfIcelandTypes.types, + loading: loading || lazyTypesLoading, error, } } diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts b/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts index 3e7cc7fc9289..5baf45b63a8d 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts @@ -41,7 +41,7 @@ const getApplicationName = (application: Application) => { const type = getValueViaPath( application.answers, - InputFields.advert.typeName, + `${InputFields.advert.type}.title`, '', ) diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts b/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts index f617921467b5..fed28c133461 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts @@ -43,6 +43,12 @@ export const regularSignatureSchema = z .array(regularSignatureItemSchema) .optional() +export const baseEntitySchema = z.object({ + id: z.string(), + title: z.string(), + slug: z.string(), +}) + export const signatureInstitutionSchema = z.enum(['institution', 'date']) export const committeeSignatureSchema = regularSignatureItemSchema @@ -61,13 +67,12 @@ export const channelSchema = z const advertSchema = z .object({ - departmentId: z.string().optional(), - typeName: z.string().optional(), - typeId: z.string().optional(), + department: baseEntitySchema.optional(), + type: baseEntitySchema.optional().nullable(), title: z.string().optional(), html: z.string().optional(), requestedDate: z.string().optional(), - categories: z.array(z.string()).optional(), + categories: z.array(baseEntitySchema).optional(), channels: z.array(channelSchema).optional(), message: z.string().optional(), additions: additionSchema.optional(), @@ -108,16 +113,16 @@ export const partialSchema = z.object({ // We make properties optional to throw custom error messages export const advertValidationSchema = z.object({ advert: z.object({ - departmentId: z - .string() + department: baseEntitySchema .optional() - .refine((value) => value && value.length > 0, { + .nullable() + .refine((value) => value !== null && value !== undefined, { params: error.missingDepartment, }), - typeId: z - .string() + type: baseEntitySchema .optional() - .refine((value) => value && value.length > 0, { + .nullable() + .refine((value) => value !== null && value !== undefined, { params: error.missingType, }), title: z @@ -137,17 +142,17 @@ export const advertValidationSchema = z.object({ export const previewValidationSchema = z.object({ advert: z.object({ - departmentId: z - .string() + department: baseEntitySchema .optional() - .refine((value) => value && value.length > 0, { - params: error.missingPreviewDepartment, + .nullable() + .refine((value) => value !== null && value !== undefined, { + params: error.missingDepartment, }), - typeId: z - .string() + type: baseEntitySchema .optional() - .refine((value) => value && value.length > 0, { - params: error.missingPreviewType, + .nullable() + .refine((value) => value !== null && value !== undefined, { + params: error.missingType, }), title: z .string() @@ -173,7 +178,7 @@ export const publishingValidationSchema = z.object({ params: error.missingRequestedDate, }), categories: z - .array(z.string()) + .array(baseEntitySchema) .optional() .refine((value) => Array.isArray(value) && value.length > 0, { params: error.noCategorySelected, diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/types.ts b/libs/application/templates/official-journal-of-iceland/src/lib/types.ts index ccae26abdb38..385aec190a50 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/types.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/types.ts @@ -11,9 +11,8 @@ export const InputFields = { approveExternalData: 'requirements.approveExternalData', }, [Routes.ADVERT]: { - departmentId: 'advert.departmentId', - typeName: 'advert.typeName', - typeId: 'advert.typeId', + department: 'advert.department', + type: 'advert.type', title: 'advert.title', html: 'advert.html', requestedDate: 'advert.requestedDate', @@ -39,8 +38,8 @@ export const InputFields = { export const RequiredInputFieldsNames = { [Routes.ADVERT]: { - departmentId: 'Deild', - typeId: 'Tegund', + department: 'Deild', + type: 'Tegund', title: 'Titill', html: 'Auglýsing', requestedDate: 'Útgáfudagur', diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts b/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts index 2e6f17b53156..96d1b2fb0893 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts @@ -3,6 +3,7 @@ import addYears from 'date-fns/addYears' import { z } from 'zod' import { additionSchema, + baseEntitySchema, committeeSignatureSchema, memberItemSchema, partialSchema, @@ -130,6 +131,11 @@ export const getSignatureDefaultValues = (signature: any, index?: number) => { return { institution: signature.institution, date: signature.date } } +export const isBaseEntity = ( + entity: unknown, +): entity is z.infer => + baseEntitySchema.safeParse(entity).success + export const isAddition = ( addition: unknown, ): addition is z.infer => diff --git a/libs/application/templates/official-journal-of-iceland/src/screens/AdvertScreen.tsx b/libs/application/templates/official-journal-of-iceland/src/screens/AdvertScreen.tsx index 56a115f61241..92d9a3911d77 100644 --- a/libs/application/templates/official-journal-of-iceland/src/screens/AdvertScreen.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/screens/AdvertScreen.tsx @@ -11,13 +11,6 @@ export const AdvertScreen = (props: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() const [modalVisible, setModalVisability] = useState(false) - const generateTimestamp = () => new Date().toISOString() - - /** - * This state here is for force rerendering of the HTML editor when a value is received from the modal - */ - const [timestamp, setTimestamp] = useState(generateTimestamp()) - return ( { } > - + setTimestamp(generateTimestamp())} + onConfirmChange={() => { + setTimeout(() => { + props.refetch && props.refetch() + }, 300) + }} /> ) diff --git a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts index 5cbb11d913b5..e1de9e93df1a 100644 --- a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts @@ -12,6 +12,7 @@ import { TicketStatus, ZendeskService, } from '@island.is/clients/zendesk' +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' import { Delegation } from '../models/delegation.model' import { DelegationAdminCustomDto } from '../dto/delegation-admin-custom.dto' @@ -45,6 +46,7 @@ export class DelegationAdminCustomService { private delegationScopeService: DelegationScopeService, private namesService: NamesService, private sequelize: Sequelize, + private rskCompanyInfoService: CompanyRegistryClientService, ) {} private getZendeskCustomFields(ticket: Ticket): { @@ -276,9 +278,7 @@ export class DelegationAdminCustomService { }) } - if ( - !(kennitala.isPerson(fromNationalId) && kennitala.isPerson(toNationalId)) - ) { + if (!kennitala.isPerson(toNationalId)) { throw new BadRequestException({ message: 'National Ids are not valid', error: ErrorCodes.INPUT_VALIDATION_INVALID_PERSON, @@ -300,7 +300,11 @@ export class DelegationAdminCustomService { }, ): Promise { const [fromDisplayName, toName] = await Promise.all([ - this.namesService.getPersonName(delegation.fromNationalId), + kennitala.isPerson(delegation.fromNationalId) + ? this.namesService.getPersonName(delegation.fromNationalId) + : this.rskCompanyInfoService + .getCompany(delegation.fromNationalId) + .then((company) => company?.name ?? ''), this.namesService.getPersonName(delegation.toNationalId), ]) @@ -310,6 +314,7 @@ export class DelegationAdminCustomService { { fromNationalId: delegation.fromNationalId, toNationalId: delegation.toNationalId, + domainName: null, }, { referenceId: delegation.referenceId, diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts index d12b1d8af8e4..bb74ed0a4d6e 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts @@ -5,6 +5,7 @@ import addDays from 'date-fns/addDays' import startOfDay from 'date-fns/startOfDay' import { Op, Transaction } from 'sequelize' import { uuid } from 'uuidv4' +import * as kennitala from 'kennitala' import { SyslumennService } from '@island.is/clients/syslumenn' import { logger } from '@island.is/logging' @@ -209,7 +210,14 @@ export class DelegationScopeService { { model: ApiScopeDelegationType, where: { - delegationType: AuthDelegationType.GeneralMandate, + delegationType: { + [Op.or]: kennitala.isCompany(fromNationalId) + ? [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.ProcurationHolder, + ] + : [AuthDelegationType.GeneralMandate], + }, }, }, ], diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts index ee60fb61c692..e11e509095bd 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts @@ -196,17 +196,48 @@ export class DelegationsIncomingCustomService { ) } + /** + * Finds all companies that have a general mandate for the user. + * @param user + * @param clientAllowedApiScopes + * @param requireApiScopes + */ + async findCompanyGeneralMandate( + user: User, + clientAllowedApiScopes: ApiScopeInfo[], + requireApiScopes: boolean, + ): Promise { + const delegations = await this.findAllAvailableGeneralMandate( + user, + clientAllowedApiScopes, + requireApiScopes, + [AuthDelegationType.ProcurationHolder], + ) + + return delegations.filter((d) => kennitala.isCompany(d.fromNationalId)) + } + + /** + * Finds all individuals that have a general mandate for the user. + * @param user + * @param clientAllowedApiScopes + * @param requireApiScopes + * @param supportedDelegationTypes + */ async findAllAvailableGeneralMandate( user: User, clientAllowedApiScopes: ApiScopeInfo[], requireApiScopes: boolean, + supportedDelegationTypes = [AuthDelegationType.GeneralMandate], ): Promise { const customApiScopes = clientAllowedApiScopes.filter( (s) => !s.isAccessControlled && this.filterByCustomScopeRule(s) && - s.supportedDelegationTypes?.some( - (dt) => dt.delegationType === AuthDelegationType.GeneralMandate, + s.supportedDelegationTypes?.some((dt) => + supportedDelegationTypes.includes( + dt.delegationType as AuthDelegationType, + ), ), ) diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts index b2e0775d3b48..05ddc050ee0f 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts @@ -176,6 +176,25 @@ export class DelegationsIncomingService { ) } + // If procuration holder is enabled, we need to get the general mandate delegations + if (types?.includes(AuthDelegationType.ProcurationHolder)) { + const isGeneralMandateDelegationEnabled = + await this.featureFlagService.getValue( + Features.isGeneralMandateDelegationEnabled, + false, + user, + ) + if (isGeneralMandateDelegationEnabled) { + delegationPromises.push( + this.delegationsIncomingCustomService.findCompanyGeneralMandate( + user, + clientAllowedApiScopes, + client.requireApiScopes, + ), + ) + } + } + if (providers.includes(AuthDelegationProvider.CompanyRegistry)) { delegationPromises.push( this.incomingDelegationsCompanyService @@ -293,6 +312,11 @@ export class DelegationsIncomingService { new Map(), ) + // Remove duplicate delegationTypes.. + mergedDelegationMap.forEach((delegation) => { + delegation.types = Array.from(new Set(delegation.types)) + }) + return [...mergedDelegationMap.values()] } diff --git a/libs/judicial-system/formatters/src/lib/formatters.ts b/libs/judicial-system/formatters/src/lib/formatters.ts index 7d9d260ddd88..91cc3013da3a 100644 --- a/libs/judicial-system/formatters/src/lib/formatters.ts +++ b/libs/judicial-system/formatters/src/lib/formatters.ts @@ -218,6 +218,29 @@ export const indictmentSubtypes: IndictmentSubtypes = { THEFT: 'þjófnaður', } +export const districtCourtAbbreviation = (courtName?: string | null) => { + switch (courtName) { + case 'Héraðsdómur Reykjavíkur': + return 'HDR' + case 'Héraðsdómur Reykjaness': + return 'HDRN' + case 'Héraðsdómur Vesturlands': + return 'HDV' + case 'Héraðsdómur Suðurlands': + return 'HDS' + case 'Héraðsdómur Norðurlands eystra': + return 'HDNE' + case 'Héraðsdómur Norðurlands vestra': + return 'HDNV' + case 'Héraðsdómur Austurlands': + return 'HDA' + case 'Héraðsdómur Vestfjarða': + return 'HDVF' + default: + return '' + } +} + export const getAppealResultTextByValue = ( value?: CaseAppealRulingDecision | null, ) => { diff --git a/libs/nest/pagination/src/lib/paginate.ts b/libs/nest/pagination/src/lib/paginate.ts index e289af92c053..87410488a3f0 100644 --- a/libs/nest/pagination/src/lib/paginate.ts +++ b/libs/nest/pagination/src/lib/paginate.ts @@ -117,6 +117,7 @@ export interface PaginateInput { primaryKeyField: string orderOption: any where?: any + attributes?: any after: string before?: string limit: number @@ -138,6 +139,7 @@ export async function paginate({ after, before, limit, + attributes, ...queryArgs }: PaginateInput): Promise<{ totalCount: number @@ -164,6 +166,7 @@ export async function paginate({ where: paginationWhere, limit, order, + attributes, ...queryArgs, } diff --git a/libs/portals/admin/service-desk/src/lib/messages.ts b/libs/portals/admin/service-desk/src/lib/messages.ts index 8560dcf08ef2..9e9c4008fcfd 100644 --- a/libs/portals/admin/service-desk/src/lib/messages.ts +++ b/libs/portals/admin/service-desk/src/lib/messages.ts @@ -65,6 +65,14 @@ export const m = defineMessages({ id: 'admin-portal.service-desk:info', defaultMessage: 'Upplýsingar: ', }, + notifications: { + id: 'admin-portal.service-desk:notifications', + defaultMessage: 'Tilkynningar: ', + }, + loadMore: { + id: 'admin-portal.service-desk:load-more', + defaultMessage: 'Sjá meira', + }, email: { id: 'admin-portal.service-desk:email', defaultMessage: 'Netfang', diff --git a/libs/portals/admin/service-desk/src/screens/User/User.graphql b/libs/portals/admin/service-desk/src/screens/User/User.graphql index 1d65b549f8b6..dd1f8410069b 100644 --- a/libs/portals/admin/service-desk/src/screens/User/User.graphql +++ b/libs/portals/admin/service-desk/src/screens/User/User.graphql @@ -32,3 +32,24 @@ mutation UpdateUserProfile( locale } } + +query GetAdminNotifications($nationalId: String!, $input: NotificationsInput!) { + adminNotifications(nationalId: $nationalId, input: $input) { + data { + id + notificationId + sender { + id + logoUrl + } + sent + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } +} diff --git a/libs/portals/admin/service-desk/src/screens/User/User.tsx b/libs/portals/admin/service-desk/src/screens/User/User.tsx index 7b16758ac36a..3a132e6512df 100644 --- a/libs/portals/admin/service-desk/src/screens/User/User.tsx +++ b/libs/portals/admin/service-desk/src/screens/User/User.tsx @@ -1,18 +1,34 @@ import format from 'date-fns/format' import { useLoaderData, useNavigate, useRevalidator } from 'react-router-dom' -import { ActionCard, Box, Stack, Text } from '@island.is/island-ui/core' +import { + ActionCard, + Box, + Stack, + Text, + Table as T, + LoadingDots, + SkeletonLoader, +} from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { BackButton } from '@island.is/portals/admin/core' import { IntroHeader, formatNationalId } from '@island.is/portals/core' import { dateFormat } from '@island.is/shared/constants' +import InfiniteScroll from 'react-infinite-scroller' -import { ServiceDeskPaths } from '../../lib/paths' -import { UserProfileResult } from './User.loader' import { m } from '../../lib/messages' -import { useUpdateUserProfileMutation } from './User.generated' +import { + GetAdminNotificationsQuery, + useUpdateUserProfileMutation, +} from './User.generated' import { UpdateUserProfileInput } from '@island.is/api/schema' import React from 'react' +import { isValidDate } from '@island.is/shared/utils' +import { useGetAdminNotificationsQuery } from './User.generated' +import { UserProfileResult } from './User.loader' +import { Problem } from '@island.is/react-spa/shared' + +const DEFAULT_PAGE_SIZE = 10 const User = () => { const { formatMessage } = useLocale() @@ -22,6 +38,18 @@ const User = () => { const [updateProfile] = useUpdateUserProfileMutation() const { revalidate } = useRevalidator() + const { + data: notifications, + loading, + error, + fetchMore, + } = useGetAdminNotificationsQuery({ + variables: { + nationalId: user.nationalId, + input: { limit: DEFAULT_PAGE_SIZE }, + }, + }) + const handleUpdateProfile = async (input: UpdateUserProfileInput) => { try { const updatedProfile = await updateProfile({ @@ -39,6 +67,38 @@ const User = () => { } } + const loadMore = async () => { + if ( + loading || + !notifications || + !notifications?.adminNotifications?.pageInfo.hasNextPage + ) { + return + } + + await fetchMore({ + variables: { + nationalId: user.nationalId, + input: { + limit: DEFAULT_PAGE_SIZE, + after: + notifications?.adminNotifications?.pageInfo.endCursor ?? undefined, + }, + }, + updateQuery: (prev, { fetchMoreResult }): GetAdminNotificationsQuery => { + return { + adminNotifications: { + ...fetchMoreResult.adminNotifications, + data: [ + ...(prev.adminNotifications?.data || []), + ...(fetchMoreResult.adminNotifications?.data || []), + ], + } as GetAdminNotificationsQuery['adminNotifications'], + } + }, + }) + } + return ( { + + {formatMessage(m.notifications)} + {error ? ( + + ) : loading ? ( + + ) : ( + + + + } + > + + + + ID + Message ID + Sender ID + Sent + + + + {notifications?.adminNotifications?.data.map( + (notification, index) => ( + + {notification.id} + {notification.notificationId} + {notification.sender.id} + + {notification.sent && + isValidDate(new Date(notification.sent)) + ? format(new Date(notification.sent), 'dd.MM.yyyy') + : ''} + + + ), + )} + + + + )} + ) } diff --git a/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts b/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts index 13454e204aaa..bfe8cbfb1104 100644 --- a/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts +++ b/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts @@ -1,23 +1,20 @@ import { z } from 'zod' -import { redirect } from 'react-router-dom' import { RawRouterActionResponse, WrappedActionFn, } from '@island.is/portals/core' import { - replaceParams, validateFormData, ValidateFormDataResult, } from '@island.is/react-spa/shared' -import { maskString, isSearchTermValid } from '@island.is/shared/utils' +import { isSearchTermValid } from '@island.is/shared/utils' import { GetPaginatedUserProfilesDocument, GetPaginatedUserProfilesQuery, type GetPaginatedUserProfilesQueryVariables, } from './Users.generated' -import { ServiceDeskPaths } from '../../lib/paths' export enum ErrorType { // Add more error types here when needed