diff --git a/apps/contentful-apps/pages/fields/admin-only-boolean-field.tsx b/apps/contentful-apps/pages/fields/admin-only-boolean-field.tsx new file mode 100644 index 000000000000..3bf79f5d25a3 --- /dev/null +++ b/apps/contentful-apps/pages/fields/admin-only-boolean-field.tsx @@ -0,0 +1,16 @@ +import { FieldExtensionSDK } from '@contentful/app-sdk' +import { Paragraph } from '@contentful/f36-components' +import { BooleanEditor } from '@contentful/field-editor-boolean' +import { useSDK } from '@contentful/react-apps-toolkit' + +const AdminOnlyBooleanField = () => { + const sdk = useSDK() + + if (!sdk.user.spaceMembership.admin) { + return (Only admins can edit this field) + } + + return +} + +export default AdminOnlyBooleanField 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 6372ac2e17d2..b5bf4659ebcc 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 @@ -11,6 +11,7 @@ import { CourtSessionType, IndictmentCaseReviewDecision, IndictmentDecision, + PunishmentType, } from '@island.is/judicial-system/types' import { Defendant } from '../../defendant' @@ -142,4 +143,10 @@ export class CaseListEntry { @Field(() => String, { nullable: true }) readonly indictmentCompletedDate?: string + + // TEMP: Use with caution! This key will never be populated. + // It was added to bypass table component type checks for a required custom sort key + // until we have a resolution on how to handle multiple defendants in the case list + @Field(() => PunishmentType, { nullable: true }) + readonly defendantsPunishmentType?: PunishmentType } diff --git a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts index 589709247bd9..437900ffb488 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts @@ -110,6 +110,9 @@ export class Defendant { @Field(() => String, { nullable: true }) readonly sentToPrisonAdminDate?: string + @Field(() => String, { nullable: true }) + readonly openedByPrisonAdminDate?: string + @Field(() => PunishmentType, { nullable: true }) readonly punishmentType?: PunishmentType } 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 75fe945637d3..ff03a3b13def 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 @@ -417,6 +417,16 @@ export const caseListInclude: Includeable[] = [ as: 'defendants', required: false, order: [['created', 'ASC']], + include: [ + { + model: DefendantEventLog, + as: 'eventLogs', + required: false, + where: { eventType: defendantEventTypes }, + order: [['created', 'DESC']], + separate: true, + }, + ], separate: true, }, { @@ -1302,18 +1312,29 @@ export class CaseService { (defendant) => defendant.id === updatedDefendant.id, )?.subpoenas?.[0]?.id !== updatedDefendant.subpoenas?.[0]?.id, ) - .map((updatedDefendant) => ({ - type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, - user, - caseId: theCase.id, - elementId: [ - updatedDefendant.id, - updatedDefendant.subpoenas?.[0].id ?? '', - ], - })) + .map((updatedDefendant) => [ + { + type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, + user, + caseId: theCase.id, + elementId: [ + updatedDefendant.id, + updatedDefendant.subpoenas?.[0].id ?? '', + ], + }, + { + type: MessageType.DELIVERY_TO_COURT_SUBPOENA, + user, + caseId: theCase.id, + elementId: [ + updatedDefendant.id, + updatedDefendant.subpoenas?.[0].id ?? '', + ], + }, + ]) if (messages && messages.length > 0) { - return this.messageService.sendMessagesToQueue(messages) + return this.messageService.sendMessagesToQueue(messages.flat()) } } @@ -1416,7 +1437,10 @@ export class CaseService { await this.addMessagesForCourtCaseConnectionToQueue(updatedCase, user) } } else { - if (updatedCase.prosecutorId !== theCase.prosecutorId) { + if ( + !isIndictment && + updatedCase.prosecutorId !== theCase.prosecutorId + ) { // New prosecutor await this.addMessagesForProsecutorChangeToQueue(updatedCase, user) } diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts index 85e302ad6754..adfb51abe7f2 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts @@ -7,6 +7,8 @@ import { NestInterceptor, } from '@nestjs/common' +import { DefendantEventType } from '@island.is/judicial-system/types' + import { Defendant, DefendantEventLog } from '../../defendant' import { Case } from '../models/case.model' import { CaseString } from '../models/caseString.model' @@ -15,8 +17,15 @@ export const transformDefendants = (defendants?: Defendant[]) => { return defendants?.map((defendant) => ({ ...defendant.toJSON(), sentToPrisonAdminDate: defendant.isSentToPrisonAdmin - ? DefendantEventLog.sentToPrisonAdminDate(defendant.eventLogs)?.created + ? DefendantEventLog.getDefendantEventLogTypeDate({ + defendantEventLogs: defendant.eventLogs, + eventType: DefendantEventType.SENT_TO_PRISON_ADMIN, + }) : undefined, + openedByPrisonAdminDate: DefendantEventLog.getDefendantEventLogTypeDate({ + defendantEventLogs: defendant.eventLogs, + eventType: DefendantEventType.OPENED_BY_PRISON_ADMIN, + }), })) } diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/defendantIndictmentAccessed.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/defendantIndictmentAccessed.interceptor.ts new file mode 100644 index 000000000000..64d122d8db7c --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/defendantIndictmentAccessed.interceptor.ts @@ -0,0 +1,63 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common' + +import { + DefendantEventType, + isIndictmentCase, + isPrisonAdminUser, + User, +} from '@island.is/judicial-system/types' + +import { DefendantEventLog, DefendantService } from '../../defendant' +import { Case } from '../models/case.model' + +const hasValidOpenByPrisonAdminEvent = ( + defendantEventLogs: DefendantEventLog[], +) => { + const sentToPrisonAdminDate = DefendantEventLog.getDefendantEventLogTypeDate({ + defendantEventLogs, + eventType: DefendantEventType.SENT_TO_PRISON_ADMIN, + }) + const openedByPrisonAdminDate = + DefendantEventLog.getDefendantEventLogTypeDate({ + defendantEventLogs, + eventType: DefendantEventType.OPENED_BY_PRISON_ADMIN, + }) + return ( + sentToPrisonAdminDate && + openedByPrisonAdminDate && + sentToPrisonAdminDate <= openedByPrisonAdminDate + ) +} + +@Injectable() +export class DefendantIndictmentAccessedInterceptor implements NestInterceptor { + constructor(private readonly defendantService: DefendantService) {} + + intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest() + const user: User = request.user + const theCase: Case = request.case + + if (isIndictmentCase(theCase.type) && isPrisonAdminUser(user)) { + const defendantsIndictmentNotOpened = theCase.defendants?.filter( + ({ isSentToPrisonAdmin, eventLogs = [] }) => + isSentToPrisonAdmin && !hasValidOpenByPrisonAdminEvent(eventLogs), + ) + + // create new events for all defendants that prison admin has not accessed according to defendant event logs + defendantsIndictmentNotOpened?.forEach((defendant) => + this.defendantService.createDefendantEvent({ + caseId: theCase.id, + defendantId: defendant.id, + eventType: DefendantEventType.OPENED_BY_PRISON_ADMIN, + }), + ) + } + return next.handle() + } +} diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts index d48a86633f14..5b65a20a1abe 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts @@ -116,7 +116,10 @@ export class InternalCaseController { ) } - @UseGuards(CaseExistsGuard) + @UseGuards( + CaseExistsGuard, + new CaseTypeGuard([...restrictionCases, ...investigationCases]), + ) @Post( `case/:caseId/${messageEndpoint[MessageType.DELIVERY_TO_COURT_PROSECUTOR]}`, ) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts index c263e83f455d..be63d588328f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts @@ -30,13 +30,16 @@ import type { User as TUser } from '@island.is/judicial-system/types' import { CaseState, CaseType, + DefendantEventType, indictmentCases, investigationCases, restrictionCases, + UserRole, } from '@island.is/judicial-system/types' import { nowFactory } from '../../factories' import { defenderRule, prisonSystemStaffRule } from '../../guards' +import { DefendantService } from '../defendant' import { EventService } from '../event' import { User } from '../user' import { TransitionCaseDto } from './dto/transitionCase.dto' @@ -57,6 +60,7 @@ import { } from './guards/rolesRules' import { CaseInterceptor } from './interceptors/case.interceptor' import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' +import { DefendantIndictmentAccessedInterceptor } from './interceptors/defendantIndictmentAccessed.interceptor' import { LimitedAccessCaseFileInterceptor } from './interceptors/limitedAccessCaseFile.interceptor' import { Case } from './models/case.model' import { transitionCase } from './state/case.state' @@ -73,6 +77,7 @@ export class LimitedAccessCaseController { private readonly limitedAccessCaseService: LimitedAccessCaseService, private readonly eventService: EventService, private readonly pdfService: PdfService, + private readonly defendantService: DefendantService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -84,6 +89,7 @@ export class LimitedAccessCaseController { ) @RolesRules(prisonSystemStaffRule, defenderRule) @UseInterceptors( + DefendantIndictmentAccessedInterceptor, CompletedAppealAccessedInterceptor, LimitedAccessCaseFileInterceptor, CaseInterceptor, @@ -100,7 +106,7 @@ export class LimitedAccessCaseController { ): Promise { this.logger.debug(`Getting limitedAccess case ${caseId} by id`) - if (!theCase.openedByDefender) { + if (user.role === UserRole.DEFENDER && !theCase.openedByDefender) { const updated = await this.limitedAccessCaseService.update( theCase, { openedByDefender: nowFactory() }, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts index b3b0bb816313..51d7fd6c5d6e 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/update.spec.ts @@ -897,12 +897,24 @@ describe('CaseController - Update', () => { caseId: theCase.id, elementId: [defendantId1, subpoenaId1], }, + { + type: MessageType.DELIVERY_TO_COURT_SUBPOENA, + user, + caseId: theCase.id, + elementId: [defendantId1, subpoenaId1], + }, { type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, user, caseId: theCase.id, elementId: [defendantId2, subpoenaId2], }, + { + type: MessageType.DELIVERY_TO_COURT_SUBPOENA, + user, + caseId: theCase.id, + elementId: [defendantId2, subpoenaId2], + }, ]) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverProsecutorToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverProsecutorToCourtGuards.spec.ts index 4e5a488ca75b..3c13895d77c9 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverProsecutorToCourtGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverProsecutorToCourtGuards.spec.ts @@ -1,6 +1,10 @@ -import { CanActivate } from '@nestjs/common' +import { + investigationCases, + restrictionCases, +} from '@island.is/judicial-system/types' import { CaseExistsGuard } from '../../guards/caseExists.guard' +import { CaseTypeGuard } from '../../guards/caseType.guard' import { InternalCaseController } from '../../internalCase.controller' describe('InternalCaseController - Deliver prosecutor to court guards', () => { @@ -14,19 +18,12 @@ describe('InternalCaseController - Deliver prosecutor to court guards', () => { ) }) - it('should have one guards', () => { - expect(guards).toHaveLength(1) - }) - - describe('CaseExistsGuard', () => { - let guard: CanActivate - - beforeEach(() => { - guard = new guards[0]() - }) - - it('should have CaseExistsGuard as guard 1', () => { - expect(guard).toBeInstanceOf(CaseExistsGuard) + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(2) + expect(new guards[0]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[1]).toBeInstanceOf(CaseTypeGuard) + expect(guards[1]).toEqual({ + allowedCaseTypes: [...restrictionCases, ...investigationCases], }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getById.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getById.spec.ts index 034bded816f0..4036446f36ed 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getById.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getById.spec.ts @@ -1,6 +1,6 @@ import { uuid } from 'uuidv4' -import type { User } from '@island.is/judicial-system/types' +import { type User, UserRole } from '@island.is/judicial-system/types' import { createTestingCaseModule } from '../createTestingCaseModule' @@ -14,14 +14,18 @@ interface Then { error: Error } -type GivenWhenThen = (caseId: string, theCase: Case) => Promise +type GivenWhenThen = ( + caseId: string, + theCase: Case, + user?: User, +) => Promise describe('LimitedAccessCaseController - Get by id', () => { let givenWhenThen: GivenWhenThen const openedBeforeDate = randomDate() const openedNowDate = randomDate() const caseId = uuid() - const user = { id: uuid() } as User + const defaultUser = { id: uuid() } as User let mockCaseModel: typeof Case @@ -42,7 +46,11 @@ describe('LimitedAccessCaseController - Get by id', () => { const mockFindOne = mockCaseModel.findOne as jest.Mock mockFindOne.mockResolvedValue(updatedCase) - givenWhenThen = async (caseId: string, theCase: Case) => { + givenWhenThen = async ( + caseId: string, + theCase: Case, + user = defaultUser, + ) => { const then = {} as Then try { @@ -79,11 +87,11 @@ describe('LimitedAccessCaseController - Get by id', () => { describe('case exists and has not been opened by defender before', () => { const theCase = { id: caseId } as Case - + const user = { ...defaultUser, role: UserRole.DEFENDER } as User let then: Then beforeEach(async () => { - then = await givenWhenThen(caseId, theCase) + then = await givenWhenThen(caseId, theCase, user) }) it('should update openedByDefender and return case', () => { diff --git a/apps/judicial-system/backend/src/app/modules/court/court.service.ts b/apps/judicial-system/backend/src/app/modules/court/court.service.ts index 33f9c74c7c83..1cfd7b5f014b 100644 --- a/apps/judicial-system/backend/src/app/modules/court/court.service.ts +++ b/apps/judicial-system/backend/src/app/modules/court/court.service.ts @@ -34,6 +34,7 @@ export enum CourtDocumentFolder { CASE_DOCUMENTS = 'Gögn málsins', COURT_DOCUMENTS = 'Dómar, úrskurðir og Þingbók', APPEAL_DOCUMENTS = 'Kæra til Landsréttar', + SUBPOENA_DOCUMENTS = 'Boðanir', } export type Subtype = Exclude | IndictmentSubtype @@ -342,6 +343,7 @@ export class CourtService { return await this.courtClientService.createCase(courtId, { caseType: isIndictment ? 'S - Ákærumál' : 'R - Rannsóknarmál', + // TODO: send a list of subtypes when CourtService supports it subtype: courtSubtype as string, status: 'Skráð', receivalDate: formatISO(receivalDate, { representation: 'date' }), diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts index 6276f0634115..d1f24d828393 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts @@ -271,6 +271,22 @@ export class DefendantService { return updatedDefendant } + async createDefendantEvent({ + caseId, + defendantId, + eventType, + }: { + caseId: string + defendantId: string + eventType: DefendantEventType + }): Promise { + await this.defendantEventLogModel.create({ + caseId, + defendantId, + eventType, + }) + } + async updateIndictmentCaseDefendant( theCase: Case, defendant: Defendant, @@ -284,7 +300,7 @@ export class DefendantService { ) if (update.isSentToPrisonAdmin) { - this.defendantEventLogModel.create({ + this.createDefendantEvent({ caseId: theCase.id, defendantId: defendant.id, eventType: DefendantEventType.SENT_TO_PRISON_ADMIN, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts index ca0a332704ee..df19027fd0f9 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts @@ -20,11 +20,17 @@ import { Defendant } from './defendant.model' timestamps: true, }) export class DefendantEventLog extends Model { - static sentToPrisonAdminDate(defendantEventLogs?: DefendantEventLog[]) { + // gets the latest log date of a given type, since the defendant event logs are sorted + static getDefendantEventLogTypeDate({ + defendantEventLogs, + eventType, + }: { + defendantEventLogs?: DefendantEventLog[] + eventType: DefendantEventType + }) { return defendantEventLogs?.find( - (defendantEventLog) => - defendantEventLog.eventType === DefendantEventType.SENT_TO_PRISON_ADMIN, - ) + (defendantEventLog) => defendantEventLog.eventType === eventType, + )?.created } @Column({ diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts index 87402f7b3ada..2937e657a3f4 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts @@ -67,7 +67,7 @@ export class InternalSubpoenaController { ) @ApiOkResponse({ type: DeliverResponse, - description: 'Delivers a subpoena to police', + description: 'Delivers a subpoena to the police', }) deliverSubpoenaToPolice( @Param('caseId') caseId: string, @@ -79,7 +79,7 @@ export class InternalSubpoenaController { @Body() deliverDto: DeliverDto, ): Promise { this.logger.debug( - `Delivering subpoena ${subpoenaId} to police for defendant ${defendantId} of case ${caseId}`, + `Delivering subpoena ${subpoenaId} pdf to police for defendant ${defendantId} of case ${caseId}`, ) return this.subpoenaService.deliverSubpoenaToPolice( @@ -89,4 +89,40 @@ export class InternalSubpoenaController { deliverDto.user, ) } + + @UseGuards( + CaseExistsGuard, + new CaseTypeGuard(indictmentCases), + DefendantExistsGuard, + SubpoenaExistsGuard, + ) + @Post( + `case/:caseId/${ + messageEndpoint[MessageType.DELIVERY_TO_COURT_SUBPOENA] + }/:defendantId/:subpoenaId`, + ) + @ApiOkResponse({ + type: DeliverResponse, + description: 'Delivers a subpoena to the court', + }) + deliverSubpoenaToCourt( + @Param('caseId') caseId: string, + @Param('defendantId') defendantId: string, + @Param('subpoenaId') subpoenaId: string, + @CurrentCase() theCase: Case, + @CurrentDefendant() defendant: Defendant, + @CurrentSubpoena() subpoena: Subpoena, + @Body() deliverDto: DeliverDto, + ): Promise { + this.logger.debug( + `Delivering subpoena ${subpoenaId} pdf to court for defendant ${defendantId} of case ${caseId}`, + ) + + return this.subpoenaService.deliverSubpoenaToCourt( + theCase, + defendant, + subpoena, + deliverDto.user, + ) + } } diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts index 31fba1e49734..05be74045e18 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts @@ -4,6 +4,7 @@ import { SequelizeModule } from '@nestjs/sequelize' import { MessageModule } from '@island.is/judicial-system/message' import { CaseModule } from '../case/case.module' +import { CourtModule } from '../court/court.module' import { DefendantModule } from '../defendant/defendant.module' import { Defendant } from '../defendant/models/defendant.model' import { EventModule } from '../event/event.module' @@ -23,6 +24,7 @@ import { SubpoenaService } from './subpoena.service' forwardRef(() => MessageModule), forwardRef(() => EventModule), forwardRef(() => DefendantModule), + CourtModule, SequelizeModule.forFeature([Subpoena, Defendant]), ], controllers: [ 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 e2d3116d4aa5..a83405e8a488 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 @@ -31,6 +31,7 @@ import { import { Case } from '../case/models/case.model' import { PdfService } from '../case/pdf.service' +import { CourtDocumentFolder, CourtService } from '../court' import { DefendantService } from '../defendant/defendant.service' import { Defendant } from '../defendant/models/defendant.model' import { EventService } from '../event' @@ -93,6 +94,7 @@ export class SubpoenaService { private readonly fileService: FileService, private readonly eventService: EventService, private readonly defendantService: DefendantService, + private readonly courtService: CourtService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -356,6 +358,41 @@ export class SubpoenaService { } } + async deliverSubpoenaToCourt( + theCase: Case, + defendant: Defendant, + subpoena: Subpoena, + user: TUser, + ): Promise { + return this.pdfService + .getSubpoenaPdf(theCase, defendant, subpoena) + .then(async (pdf) => { + const fileName = `Fyrirkall - ${defendant.name}` + + return this.courtService.createDocument( + user, + theCase.id, + theCase.courtId, + theCase.courtCaseNumber, + CourtDocumentFolder.SUBPOENA_DOCUMENTS, + fileName, + `${fileName}.pdf`, + 'application/pdf', + pdf, + ) + }) + .then(() => ({ delivered: true })) + .catch((reason) => { + // Tolerate failure, but log error + this.logger.warn( + `Failed to upload subpoena ${subpoena.id} pdf to court for defendant ${defendant.id} of case ${theCase.id}`, + { reason }, + ) + + return { delivered: false } + }) + } + async getSubpoena(subpoena: Subpoena, user?: TUser): Promise { if (!subpoena.subpoenaId) { // The subpoena has not been delivered to the police diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts index e397c3e498ac..c4982ece5817 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/createTestingSubpoenaModule.ts @@ -13,6 +13,7 @@ import { import { MessageService } from '@island.is/judicial-system/message' import { CaseService, PdfService } from '../../case' +import { CourtService } from '../../court' import { Defendant, DefendantService } from '../../defendant' import { EventService } from '../../event' import { FileService } from '../../file' @@ -24,6 +25,7 @@ import { Subpoena } from '../models/subpoena.model' import { SubpoenaController } from '../subpoena.controller' import { SubpoenaService } from '../subpoena.service' +jest.mock('@island.is/judicial-system/message') jest.mock('../../user/user.service') jest.mock('../../case/case.service') jest.mock('../../case/pdf.service') @@ -31,7 +33,7 @@ jest.mock('../../police/police.service') jest.mock('../../file/file.service') jest.mock('../../event/event.service') jest.mock('../../defendant/defendant.service') -jest.mock('@island.is/judicial-system/message') +jest.mock('../../court/court.service') export const createTestingSubpoenaModule = async () => { const subpoenaModule = await Test.createTestingModule({ @@ -51,6 +53,7 @@ export const createTestingSubpoenaModule = async () => { FileService, EventService, DefendantService, + CourtService, { provide: LOGGER_PROVIDER, useValue: { @@ -94,6 +97,8 @@ export const createTestingSubpoenaModule = async () => { const fileService = subpoenaModule.get(FileService) + const courtService = subpoenaModule.get(CourtService) + const subpoenaModel = await subpoenaModule.resolve( getModelToken(Subpoena), ) @@ -118,6 +123,7 @@ export const createTestingSubpoenaModule = async () => { pdfService, policeService, fileService, + courtService, subpoenaModel, subpoenaService, subpoenaController, diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoanaToCourtGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoanaToCourtGuards.spec.ts new file mode 100644 index 000000000000..448185532c87 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoanaToCourtGuards.spec.ts @@ -0,0 +1,29 @@ +import { indictmentCases } from '@island.is/judicial-system/types' + +import { CaseExistsGuard, CaseTypeGuard } from '../../../case' +import { DefendantExistsGuard } from '../../../defendant' +import { SubpoenaExistsGuard } from '../../guards/subpoenaExists.guard' +import { InternalSubpoenaController } from '../../internalSubpoena.controller' + +describe('InternalSubpoenaController - Deliver subpoena to court guards', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let guards: any[] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + InternalSubpoenaController.prototype.deliverSubpoenaToCourt, + ) + }) + + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(4) + expect(new guards[0]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[1]).toBeInstanceOf(CaseTypeGuard) + expect(guards[1]).toEqual({ + allowedCaseTypes: indictmentCases, + }) + expect(new guards[2]()).toBeInstanceOf(DefendantExistsGuard) + expect(new guards[3]()).toBeInstanceOf(SubpoenaExistsGuard) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoanaToPoliceGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoanaToPoliceGuards.spec.ts new file mode 100644 index 000000000000..4f1663e3cc03 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoanaToPoliceGuards.spec.ts @@ -0,0 +1,29 @@ +import { indictmentCases } from '@island.is/judicial-system/types' + +import { CaseExistsGuard, CaseTypeGuard } from '../../../case' +import { DefendantExistsGuard } from '../../../defendant' +import { SubpoenaExistsGuard } from '../../guards/subpoenaExists.guard' +import { InternalSubpoenaController } from '../../internalSubpoena.controller' + +describe('InternalSubpoenaController - Deliver subpoena to police guards', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let guards: any[] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + InternalSubpoenaController.prototype.deliverSubpoenaToPolice, + ) + }) + + it('should have the right guard configuration', () => { + expect(guards).toHaveLength(4) + expect(new guards[0]()).toBeInstanceOf(CaseExistsGuard) + expect(guards[1]).toBeInstanceOf(CaseTypeGuard) + expect(guards[1]).toEqual({ + allowedCaseTypes: indictmentCases, + }) + expect(new guards[2]()).toBeInstanceOf(DefendantExistsGuard) + expect(new guards[3]()).toBeInstanceOf(SubpoenaExistsGuard) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToCourt.spec.ts new file mode 100644 index 000000000000..1601f396fc59 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToCourt.spec.ts @@ -0,0 +1,111 @@ +import { uuid } from 'uuidv4' + +import { createTestingSubpoenaModule } from '../createTestingSubpoenaModule' + +import { Case, PdfService } from '../../../case' +import { CourtService } from '../../../court' +import { Defendant } from '../../../defendant' +import { DeliverDto } from '../../dto/deliver.dto' +import { DeliverResponse } from '../../models/deliver.response' +import { Subpoena } from '../../models/subpoena.model' + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = () => Promise + +describe('InternalSubpoenaController - Deliver subpoena to court', () => { + const caseId = uuid() + const courtId = uuid() + const courtCaseNumber = uuid() + const subpoenaId = uuid() + const defendantId = uuid() + const defendantName = uuid() + + const subpoena = { id: subpoenaId } as Subpoena + const defendant = { + id: defendantId, + name: defendantName, + subpoenas: [subpoena], + } as Defendant + const theCase = { + id: caseId, + courtId, + courtCaseNumber, + defendants: [defendant], + } as Case + const user = { id: uuid() } + const dto = { user } as DeliverDto + + let mockPdfService: PdfService + let mockCourtService: CourtService + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { courtService, pdfService, internalSubpoenaController } = + await createTestingSubpoenaModule() + + mockPdfService = pdfService + const mockGetSubpoenaPdf = mockPdfService.getSubpoenaPdf as jest.Mock + mockGetSubpoenaPdf.mockRejectedValue(new Error('Some error')) + + mockCourtService = courtService + const mockCreateDocument = mockCourtService.createDocument as jest.Mock + mockCreateDocument.mockRejectedValue(new Error('Some error')) + + givenWhenThen = async () => { + const then = {} as Then + + await internalSubpoenaController + .deliverSubpoenaToCourt( + caseId, + defendantId, + subpoenaId, + theCase, + defendant, + subpoena, + dto, + ) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('subpoena delivered to court', () => { + const subpoenaPdf = uuid() + let then: Then + + beforeEach(async () => { + const mockGetSubpoenaPdf = mockPdfService.getSubpoenaPdf as jest.Mock + mockGetSubpoenaPdf.mockResolvedValue(subpoenaPdf) + const mockCreateDocument = mockCourtService.createDocument as jest.Mock + mockCreateDocument.mockResolvedValue('') + + then = await givenWhenThen() + }) + + it('should deliver the subpoena', () => { + expect(mockPdfService.getSubpoenaPdf).toBeCalledWith( + theCase, + defendant, + subpoena, + ) + expect(mockCourtService.createDocument).toBeCalledWith( + user, + caseId, + courtId, + courtCaseNumber, + 'Boðanir', + `Fyrirkall - ${defendantName}`, + `Fyrirkall - ${defendantName}.pdf`, + 'application/pdf', + subpoenaPdf, + ) + expect(then.result).toEqual({ delivered: true }) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts index 43255bd30aa1..ff02a709d797 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/deliverSubpoenaToPolice.spec.ts @@ -2,14 +2,14 @@ import { uuid } from 'uuidv4' import { createTestingSubpoenaModule } from '../createTestingSubpoenaModule' -import { Case } from '../../../case' +import { Case, PdfService } from '../../../case' import { Defendant } from '../../../defendant' import { DeliverDto } from '../../dto/deliver.dto' import { DeliverResponse } from '../../models/deliver.response' import { Subpoena } from '../../models/subpoena.model' -import { SubpoenaService } from '../../subpoena.service' interface Then { + result: DeliverResponse error: Error } @@ -22,57 +22,59 @@ describe('InternalSubpoenaController - Deliver subpoena to police', () => { const subpoena = { id: subpoenaId } as Subpoena const defendant = { id: defendantId, subpoenas: [subpoena] } as Defendant - const theCase = { id: caseId } as Case - const user = { user: { id: uuid() } } as DeliverDto - const delivered = { delivered: true } as DeliverResponse + const theCase = { id: caseId, defendants: [defendant] } as Case + const user = { id: uuid() } + const dto = { user } as DeliverDto - let mockSubpoenaService: SubpoenaService + let mockPdfService: PdfService let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { subpoenaService, internalSubpoenaController } = + const { pdfService, internalSubpoenaController } = await createTestingSubpoenaModule() - mockSubpoenaService = subpoenaService - - const deliverSubpoenaToPoliceMock = jest.fn() - mockSubpoenaService.deliverSubpoenaToPolice = deliverSubpoenaToPoliceMock - - deliverSubpoenaToPoliceMock.mockResolvedValueOnce(delivered) + mockPdfService = pdfService + const mockGetSubpoenaPdf = mockPdfService.getSubpoenaPdf as jest.Mock + mockGetSubpoenaPdf.mockRejectedValue(new Error('Some error')) givenWhenThen = async () => { const then = {} as Then - try { - await internalSubpoenaController.deliverSubpoenaToPolice( + await internalSubpoenaController + .deliverSubpoenaToPolice( caseId, defendantId, subpoenaId, theCase, defendant, subpoena, - user, + dto, ) - } catch (error) { - then.error = error as Error - } + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) return then } }) describe('subpoena delivered to police', () => { + const subpoenaPdf = uuid() + let then: Then + beforeEach(async () => { - await givenWhenThen() + const mockGetSubpoenaPdf = mockPdfService.getSubpoenaPdf as jest.Mock + mockGetSubpoenaPdf.mockResolvedValue(subpoenaPdf) + + then = await givenWhenThen() }) it('should call deliverSubpoenaToPolice', () => { - expect(mockSubpoenaService.deliverSubpoenaToPolice).toHaveBeenCalledWith( + expect(mockPdfService.getSubpoenaPdf).toBeCalledWith( theCase, defendant, subpoena, - user.user, ) + // TODO: complete tests when all indictments are generated }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/updateSubpoeanaGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/updateSubpoeanaGuards.spec.ts index 1cad7bd28cdb..33106b7cc5a8 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/updateSubpoeanaGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/internalSubpoenaController/updateSubpoeanaGuards.spec.ts @@ -1,7 +1,7 @@ import { PoliceSubpoenaExistsGuard } from '../../guards/policeSubpoenaExists.guard' import { InternalSubpoenaController } from '../../internalSubpoena.controller' -describe('InternalSubpoenaController - Update subpoena guards', () => { +describe('InternalSubpoenaController - Update subpoena guards', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let guards: any[] diff --git a/apps/judicial-system/web/messages/Core/tables.ts b/apps/judicial-system/web/messages/Core/tables.ts index b2b2500d65bb..c17703f0fbf6 100644 --- a/apps/judicial-system/web/messages/Core/tables.ts +++ b/apps/judicial-system/web/messages/Core/tables.ts @@ -113,6 +113,11 @@ export const tables = defineMessages({ defaultMessage: 'Dómstóll', description: 'Notaður sem titill fyrir dómstóll dálk í lista yfir mál.', }, + punishmentType: { + id: 'judicial.system.core:tables.punishment_type', + defaultMessage: 'Refsitegund', + description: 'Notaður sem titill fyrir refsitegund dálk í lista yfir mál.', + }, sentencingDate: { id: 'judicial.system.core:tables.sentencing_date', defaultMessage: 'Dómsuppkvaðning', diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index 4aec6d983d4b..069e36645c3f 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -35,6 +35,7 @@ query Case($input: CaseQueryInput!) { subpoenaType isSentToPrisonAdmin sentToPrisonAdminDate + openedByPrisonAdminDate punishmentType subpoenas { id diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index dea05680c538..8d31030894bc 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -47,6 +47,7 @@ query LimitedAccessCase($input: CaseQueryInput!) { subpoenaType isSentToPrisonAdmin sentToPrisonAdminDate + openedByPrisonAdminDate punishmentType subpoenas { id diff --git a/apps/judicial-system/web/src/components/Table/Table.tsx b/apps/judicial-system/web/src/components/Table/Table.tsx index 8e572422391c..13e6a3cad088 100644 --- a/apps/judicial-system/web/src/components/Table/Table.tsx +++ b/apps/judicial-system/web/src/components/Table/Table.tsx @@ -180,6 +180,8 @@ const Table: FC = (props) => { switch (column) { case 'defendants': return entry.defendants?.[0]?.name ?? '' + case 'defendantsPunishmentType': + return entry.defendants?.[0]?.punishmentType ?? '' case 'courtCaseNumber': return courtAbbreviation ? `${courtAbbreviation}: ${entry.courtCaseNumber}` diff --git a/apps/judicial-system/web/src/components/Tags/CaseTag.strings.ts b/apps/judicial-system/web/src/components/Tags/CaseTag.strings.ts index 28f7807d82f3..0119c6c284ab 100644 --- a/apps/judicial-system/web/src/components/Tags/CaseTag.strings.ts +++ b/apps/judicial-system/web/src/components/Tags/CaseTag.strings.ts @@ -97,4 +97,29 @@ export const strings = defineMessages({ defaultMessage: 'Afturkallað', description: 'Notað fyrir "Afturkallað" tagg', }, + punishmentTypeImprisonment: { + id: 'judicial.system.core:case_tag.punishment_type_imprisonment', + defaultMessage: 'Óskb.', + description: 'Notað fyrir "Óskilorðsbundið" tagg', + }, + punishmentTypeProbation: { + id: 'judicial.system.core:case_tag.punishment_type_probation', + defaultMessage: 'Skb.', + description: 'Notað fyrir "Skilorðsbundið" tagg', + }, + punishmentTypeFine: { + id: 'judicial.system.core:case_tag.punishment_type_fine', + defaultMessage: 'Sekt', + description: 'Notað fyrir "Sekt" tagg', + }, + punishmentTypeIndictmentRulingDecisionFine: { + id: 'judicial.system.core:case_tag.punishment_type_indictment_ruling_decision_fine', + defaultMessage: 'VL', + description: 'Notað fyrir "Viðurlagaákvörðun" tagg', + }, + punishmentTypeSignedFineInvitation: { + id: 'judicial.system.core:case_tag.punishment_type_signed_fine_invitation', + defaultMessage: 'ÁS', + description: 'Notað fyrir "Áritað sektarboð" tagg', + }, }) diff --git a/apps/judicial-system/web/src/components/Tags/utils.ts b/apps/judicial-system/web/src/components/Tags/utils.ts index e4f1b3ac9f83..51cc830a199e 100644 --- a/apps/judicial-system/web/src/components/Tags/utils.ts +++ b/apps/judicial-system/web/src/components/Tags/utils.ts @@ -2,6 +2,7 @@ import { TagVariant } from '@island.is/island-ui/core' import { isDistrictCourtUser, isPublicProsecutorUser, + PunishmentType, } from '@island.is/judicial-system/types' import { @@ -119,3 +120,50 @@ export const getIndictmentRulingDecisionTag = ( return { color: 'darkerBlue', text: strings.complete } } } + +export const getPunishmentTypeTag = ( + punishmentType?: PunishmentType | null, +): { + color: TagVariant + text: { id: string; defaultMessage: string; description: string } +} | null => { + if (!punishmentType) return null + + const getPunishmentTypeLabel = (punishmentType?: PunishmentType | null) => { + switch (punishmentType) { + case PunishmentType.IMPRISONMENT: + return strings.punishmentTypeImprisonment + case PunishmentType.PROBATION: + return strings.punishmentTypeProbation + case PunishmentType.FINE: + return strings.punishmentTypeFine + case PunishmentType.INDICTMENT_RULING_DECISION_FINE: + return strings.punishmentTypeIndictmentRulingDecisionFine + case PunishmentType.SIGNED_FINE_INVITATION: + return strings.punishmentTypeSignedFineInvitation + default: + return strings.unknown + } + } + + return { + color: 'red' as TagVariant, + text: getPunishmentTypeLabel(punishmentType), + } +} + +export const getPrisonCaseStateTag = ( + prisonCaseState: CaseState, +): { + color: TagVariant + text: { id: string; defaultMessage: string; description: string } +} => { + switch (prisonCaseState) { + case CaseState.NEW: + return { color: 'purple', text: strings.new } + case CaseState.RECEIVED: + return { color: 'blue', text: strings.received } + default: + return { color: 'darkerBlue', text: strings.complete } + } +} diff --git a/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx b/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx index 169584028e9b..7a52c93d46ff 100644 --- a/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx +++ b/apps/judicial-system/web/src/routes/Defender/Cases/components/DefenderCasesTable.tsx @@ -57,6 +57,9 @@ export const DefenderCasesTable: FC = ({ ) { return entry.defendants[0].name ?? '' } + if (column === 'courtDate') { + return entry.courtDate + } return entry.created } const { sortedData, requestSort, getClassNamesFor, isActiveColumn } = useSort( @@ -129,9 +132,13 @@ export const DefenderCasesTable: FC = ({ ) : ( - - {formatMessage(tables.hearingArrangementDate)} - + requestSort('courtDate')} + sortAsc={getClassNamesFor('courtDate') === 'ascending'} + sortDes={getClassNamesFor('courtDate') === 'descending'} + isActive={isActiveColumn('courtDate')} + /> )} diff --git a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.strings.ts b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.strings.ts index d574238883e5..bc87e97beef8 100644 --- a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.strings.ts +++ b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.strings.ts @@ -14,7 +14,12 @@ export const strings = defineMessages({ indictmentCompletedTitle: { id: 'judicial.system.core:indictment_overview.indictment_completed_title', defaultMessage: 'Dómsuppkvaðning {date}', - description: 'Titill á yfirliti ákæru fyrir fangelsi', + description: 'Undirtitill á yfirliti ákæru fyrir fangelsi', + }, + indictmentReceivedTitle: { + id: 'judicial.system.core:indictment_overview.indictment_received_title', + defaultMessage: 'Móttekið {date}', + description: 'Undirtitill á yfirliti ákæru fyrir fangelsi', }, infoCardDefendantsTitle: { id: 'judicial.system.core:indictment_overview.info_card_defendants_title', diff --git a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx index beab42f136a8..82065184df56 100644 --- a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx +++ b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx @@ -81,6 +81,13 @@ const IndictmentOverview = () => { })} )} + {defendant?.openedByPrisonAdminDate && ( + + {formatMessage(strings.indictmentReceivedTitle, { + date: formatDate(defendant.openedByPrisonAdminDate, 'PPP'), + })} + + )} diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx index 36abf477db77..5c6980497124 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx @@ -121,48 +121,50 @@ export const Overview = () => { - - - {fm(strings.reviewerSubtitle, { - isFine: - workingCase.indictmentRulingDecision === - CaseIndictmentRulingDecision.FINE, - indictmentAppealDeadline: formatDate( - workingCase.indictmentAppealDeadline, - ), - appealDeadlineIsInThePast: - workingCase.indictmentVerdictAppealDeadlineExpired, - })} - - } - /> - - { + setSelectedIndictmentReviewer(value as Option) + }} + isDisabled={loading} + required + /> + + + )} { { title: formatMessage(tables.court), }, + { + title: formatMessage(tables.punishmentType), + sortable: { isSortable: true, key: 'defendantsPunishmentType' }, + }, { title: capitalize(formatMessage(tables.sentencingDate)), }, @@ -211,17 +221,43 @@ export const PrisonCases: FC = () => { { cell: (row) => , }, + { + cell: (row) => { + const punishmentType = isNonEmptyArray(row.defendants) + ? row.defendants[0].punishmentType + : undefined + const punishmentTypeTag = getPunishmentTypeTag(punishmentType) + return punishmentTypeTag ? ( + + ) : null + }, + }, { cell: (row) => ( ), }, { - cell: () => ( - - {'Nýtt'} - - ), + cell: (row) => { + const prisonCaseState = + row.defendants && + row.defendants?.length > 0 && + row.defendants[0].openedByPrisonAdminDate + ? CaseState.RECEIVED + : CaseState.NEW + const prisonCaseStateTag = + getPrisonCaseStateTag(prisonCaseState) + + return ( + + ) + }, }, ]} generateContextMenuItems={(row) => [openCaseInNewTabMenuItem(row.id)]} 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 4001b83c6fcb..b188cbfac1aa 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql +++ b/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql @@ -28,7 +28,10 @@ query Cases { defenderChoice verdictViewDate isSentToPrisonAdmin + punishmentType + openedByPrisonAdminDate } + defendantsPunishmentType courtDate isValidToDateInThePast initialRulingDate diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql b/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql index 04f635815e1e..98b6483bff99 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql +++ b/apps/judicial-system/web/src/routes/Shared/Cases/prisonCases.graphql @@ -26,6 +26,8 @@ query PrisonCases { name noNationalId defenderChoice + punishmentType + openedByPrisonAdminDate } courtDate isValidToDateInThePast diff --git a/apps/judicial-system/web/src/utils/arrayHelpers.spec.ts b/apps/judicial-system/web/src/utils/arrayHelpers.spec.ts new file mode 100644 index 000000000000..fa49af5eefe5 --- /dev/null +++ b/apps/judicial-system/web/src/utils/arrayHelpers.spec.ts @@ -0,0 +1,45 @@ +import { isEmptyArray, isNonEmptyArray, isPresentArray } from './arrayHelpers' + +describe('arrayHelpers', () => { + describe('isPresentArray', () => { + const testCases = [ + { input: undefined, expected: false }, + { input: null, expected: false }, + { input: [], expected: true }, + { input: [1], expected: true }, + ] + testCases.forEach(({ input, expected }) => { + it(`should return ${expected} for input ${input}`, () => { + expect(isPresentArray(input)).toBe(expected) + }) + }) + }) + + describe('isEmptyArray', () => { + const testCases = [ + { input: undefined, expected: false }, + { input: null, expected: false }, + { input: [], expected: true }, + { input: [1], expected: false }, + ] + testCases.forEach(({ input, expected }) => { + it(`should return ${expected} for input ${input}`, () => { + expect(isEmptyArray(input)).toBe(expected) + }) + }) + }) + + describe('isNonEmptyArray', () => { + const testCases = [ + { input: undefined, expected: false }, + { input: null, expected: false }, + { input: [], expected: false }, + { input: [1], expected: true }, + ] + testCases.forEach(({ input, expected }) => { + it(`should return ${expected} for input ${input}`, () => { + expect(isNonEmptyArray(input)).toBe(expected) + }) + }) + }) +}) diff --git a/apps/judicial-system/web/src/utils/arrayHelpers.ts b/apps/judicial-system/web/src/utils/arrayHelpers.ts new file mode 100644 index 000000000000..8b2d62131b7b --- /dev/null +++ b/apps/judicial-system/web/src/utils/arrayHelpers.ts @@ -0,0 +1,8 @@ +export const isPresentArray = (arr: T[] | undefined | null): arr is T[] => + arr !== undefined && arr !== null && Array.isArray(arr) + +export const isEmptyArray = (arr: T[] | undefined | null): arr is T[] => + isPresentArray(arr) && arr?.length === 0 + +export const isNonEmptyArray = (arr: T[] | undefined | null): arr is T[] => + isPresentArray(arr) && arr.length > 0 diff --git a/apps/web/components/Organization/Slice/OverviewLinks/OverviewLinks.tsx b/apps/web/components/Organization/Slice/OverviewLinks/OverviewLinks.tsx index 8b1a4604f4e3..1decf6b1bf48 100644 --- a/apps/web/components/Organization/Slice/OverviewLinks/OverviewLinks.tsx +++ b/apps/web/components/Organization/Slice/OverviewLinks/OverviewLinks.tsx @@ -55,23 +55,18 @@ export const OverviewLinksSlice: React.FC< key={index} direction={leftImage ? 'row' : 'rowReverse'} > - - - {/** - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error make web strict */} - - - + {image?.url && ( + + + + + + )} + > + {!indexableBySearchEngine && ( + + )} + { const formattedStartDate = format( new Date(item.startDate), - 'dd. MMMM yyyy', + 'd. MMMM yyyy', { locale: localeMap[locale], }, diff --git a/apps/web/screens/Article/Article.tsx b/apps/web/screens/Article/Article.tsx index 1ca78a49e537..16bfd88f3b59 100644 --- a/apps/web/screens/Article/Article.tsx +++ b/apps/web/screens/Article/Article.tsx @@ -262,12 +262,14 @@ interface ArticleSidebarProps { article: Article activeSlug?: string | string[] n: (s: string) => string + organizationNamespace: Record } const ArticleSidebar: FC> = ({ article, activeSlug, n, + organizationNamespace, }) => { const { linkResolver } = useLinkResolver() const { activeLocale } = useI18n() @@ -296,7 +298,9 @@ const ArticleSidebar: FC> = ({ {article?.organization && article.organization.length > 0 && ( > = ({ /> )} {article?.subArticles && article.subArticles.length > 0 && ( - + )} []; slug: string }[] stepperNamespace: GetNamespaceQuery['getNamespace'] + organizationNamespace: Record } const ArticleScreen: Screen = ({ @@ -332,6 +342,7 @@ const ArticleScreen: Screen = ({ namespace, stepperNamespace, stepOptionsFromNamespace, + organizationNamespace, }) => { const { activeLocale } = useI18n() const portalRef = useRef() @@ -540,7 +551,8 @@ const ArticleScreen: Screen = ({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error make web strict title: article.organization[0].title, - label: n('organization'), + label: + organizationNamespace?.['organization'] ?? n('organization'), // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error make web strict href: getOrganizationLink( @@ -616,6 +628,7 @@ const ArticleScreen: Screen = ({ article={article} n={n} activeSlug={query.subSlug} + organizationNamespace={organizationNamespace} /> } @@ -648,6 +661,7 @@ const ArticleScreen: Screen = ({ n={n} activeSlug={query.subSlug} isMenuDialog + organizationNamespace={organizationNamespace} /> )} @@ -774,6 +788,7 @@ const ArticleScreen: Screen = ({ n={n} activeSlug={query.subSlug} isMenuDialog + organizationNamespace={organizationNamespace} /> )} @@ -910,7 +925,7 @@ ArticleScreen.getProps = async ({ apolloClient, query, locale }) => { const organizationNamespace = extractNamespaceFromOrganization( article.organization?.[0], - ) + ) as Record return { article, @@ -918,6 +933,7 @@ ArticleScreen.getProps = async ({ apolloClient, query, locale }) => { stepOptionsFromNamespace, stepperNamespace, customTopLoginButtonItem: organizationNamespace?.customTopLoginButtonItem, + organizationNamespace, ...getThemeConfig(article), } } diff --git a/apps/web/screens/Grants/Grant/Grant.css.ts b/apps/web/screens/Grants/Grant/Grant.css.ts new file mode 100644 index 000000000000..6c18ae3d5e8f --- /dev/null +++ b/apps/web/screens/Grants/Grant/Grant.css.ts @@ -0,0 +1,7 @@ +import { globalStyle, style } from '@vanilla-extract/css' + +export const noUnderline = style({}) + +globalStyle(`${noUnderline} span`, { + boxShadow: 'none', +}) diff --git a/apps/web/screens/Grants/Grant/Grant.tsx b/apps/web/screens/Grants/Grant/Grant.tsx index 03184ecdfccc..27bcc2ba1020 100644 --- a/apps/web/screens/Grants/Grant/Grant.tsx +++ b/apps/web/screens/Grants/Grant/Grant.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { useIntl } from 'react-intl' import NextLink from 'next/link' import { useRouter } from 'next/router' @@ -8,11 +9,12 @@ import { Box, Breadcrumbs, Divider, + Hidden, Stack, Text, } from '@island.is/island-ui/core' import { Locale } from '@island.is/shared/types' -import { GrantWrapper } from '@island.is/web/components' +import { GrantWrapper, InstitutionPanel } from '@island.is/web/components' import { ContentLanguage, CustomPageUniqueIdentifier, @@ -32,7 +34,10 @@ import { import SidebarLayout from '../../Layouts/SidebarLayout' import { GET_GRANT_QUERY } from '../../queries' import { m } from '../messages' -import { GrantSidebar } from './GrantSidebar' +import { generateStatusTag, parseStatus } from '../utils' +import DetailPanel from './GrantSidebar/DetailPanel' +import ExtraPanel from './GrantSidebar/ExtraPanel' +import { GrantSidebar } from './GrantSidebar/GrantSidebar' const GrantSinglePage: CustomScreen = ({ grant, locale }) => { const { formatMessage } = useIntl() @@ -67,6 +72,11 @@ const GrantSinglePage: CustomScreen = ({ grant, locale }) => { }, ] + const status = useMemo( + () => (grant ? parseStatus(grant, formatMessage, locale) : null), + [grant, formatMessage, locale], + ) + if (!grant) { return null } @@ -100,21 +110,37 @@ const GrantSinglePage: CustomScreen = ({ grant, locale }) => { {grant.description} - router.push(grant.applicationUrl?.slug ?? ''), - icon: 'open', - iconType: 'outline', - }} - /> + + + + + router.push(grant.applicationUrl?.slug ?? ''), + icon: 'open', + iconType: 'outline', + }} + /> + {grant.specialEmphasis?.length ? ( <> + {status?.note && {status.note}} {webRichText( grant.specialEmphasis as SliceType[], undefined, @@ -164,6 +190,20 @@ const GrantSinglePage: CustomScreen = ({ grant, locale }) => { )} ) : undefined} + + + + + + diff --git a/apps/web/screens/Grants/Grant/GrantSidebar.tsx b/apps/web/screens/Grants/Grant/GrantSidebar.tsx deleted file mode 100644 index 5a1941ee5b2a..000000000000 --- a/apps/web/screens/Grants/Grant/GrantSidebar.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useMemo } from 'react' -import { useIntl } from 'react-intl' - -import { - Box, - BoxProps, - Button, - LinkV2, - Stack, - Text, -} from '@island.is/island-ui/core' -import { Locale } from '@island.is/shared/types' -import { isDefined } from '@island.is/shared/utils' -import { InstitutionPanel } from '@island.is/web/components' -import { Grant } from '@island.is/web/graphql/schema' -import { LinkType, useLinkResolver } from '@island.is/web/hooks' - -import { m } from '../messages' -import { generateStatusTag, parseStatus } from '../utils' - -interface Props { - grant: Grant - locale: Locale -} - -const generateLine = (heading: string, content?: React.ReactNode) => { - if (!content) { - return null - } - return ( - - - {heading} - - {content} - - ) -} - -const generateSidebarPanel = ( - data: Array, - background: BoxProps['background'], -) => { - if (!data.length) { - return undefined - } - return ( - - {data} - - ) -} - -export const GrantSidebar = ({ grant, locale }: Props) => { - const { linkResolver } = useLinkResolver() - const { formatMessage } = useIntl() - - const status = useMemo( - () => parseStatus(grant, formatMessage, locale), - [grant, formatMessage, locale], - ) - - const detailPanelData = useMemo( - () => - [ - generateLine( - formatMessage(m.single.fund), - grant?.fund?.link?.slug ? ( - - - {grant.fund.title} - - - ) : undefined, - ), - generateLine( - formatMessage(m.single.category), - grant?.categoryTags ? ( - - {grant.categoryTags - .map((ct) => ct.title) - .filter(isDefined) - .join(', ')} - - ) : undefined, - ), - generateLine( - formatMessage(m.single.type), - grant?.typeTag?.title ? ( - {grant.typeTag?.title} - ) : undefined, - ), - generateLine( - formatMessage(m.single.status), - grant?.status ? ( - - { - generateStatusTag(status.applicationStatus, formatMessage) - ?.label - } - - ) : undefined, - ), - ].filter(isDefined) ?? [], - [grant, formatMessage, linkResolver, status], - ) - - const filesPanelData = useMemo( - () => - grant.files - ?.map((f, index) => { - if (!f.url) { - return null - } - return ( - - - - ) - }) - .filter(isDefined) ?? [], - [grant.files], - ) - - const supportLinksPanelData = useMemo( - () => - grant.supportLinks - ?.map((link) => { - if (!link.url || !link.text || !link.id) { - return null - } - return ( - - - - ) - }) - .filter(isDefined) ?? [], - [grant.supportLinks], - ) - - return ( - - - {generateSidebarPanel(detailPanelData, 'blue100')} - {generateSidebarPanel(filesPanelData, 'red100')} - {generateSidebarPanel(supportLinksPanelData, 'purple100')} - - ) -} diff --git a/apps/web/screens/Grants/Grant/GrantSidebar/DetailPanel.tsx b/apps/web/screens/Grants/Grant/GrantSidebar/DetailPanel.tsx new file mode 100644 index 000000000000..f6e78a0a6061 --- /dev/null +++ b/apps/web/screens/Grants/Grant/GrantSidebar/DetailPanel.tsx @@ -0,0 +1,116 @@ +import { useMemo } from 'react' +import { useIntl } from 'react-intl' +import { useRouter } from 'next/router' + +import { Box, Button, LinkV2, Stack, Text } from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' +import { isDefined } from '@island.is/shared/utils' +import { Grant } from '@island.is/web/graphql/schema' +import { LinkType, useLinkResolver } from '@island.is/web/hooks' + +import { m } from '../../messages' +import { generateStatusTag, parseStatus } from '../../utils' +import { generateLine } from './PanelLine' + +interface DetailPanelProps { + grant: Grant + locale: Locale + button?: boolean +} + +const DetailPanel: React.FC = ({ + grant, + locale, + button = false, +}) => { + const { linkResolver } = useLinkResolver() + const { formatMessage } = useIntl() + const router = useRouter() + + const status = useMemo( + () => parseStatus(grant, formatMessage, locale), + [grant, formatMessage, locale], + ) + + const detailPanelData = useMemo( + () => + [ + generateLine( + formatMessage(m.single.fund), + grant?.fund?.link?.slug ? ( + + + {grant.fund.title} + + + ) : undefined, + ), + generateLine( + formatMessage(m.single.status), + grant?.status ? ( + + { + generateStatusTag(status.applicationStatus, formatMessage) + ?.label + } + + ) : undefined, + ), + generateLine( + formatMessage(m.single.deadline), + status.deadlineStatus ? ( + {status.deadlineStatus} + ) : undefined, + ), + generateLine( + formatMessage(m.single.category), + grant?.categoryTags ? ( + + {grant.categoryTags + .map((ct) => ct.title) + .filter(isDefined) + .join(', ')} + + ) : undefined, + ), + generateLine( + formatMessage(m.single.type), + grant?.typeTag?.title ? ( + {grant.typeTag?.title} + ) : undefined, + ), + ].filter(isDefined) ?? [], + [grant, formatMessage, linkResolver, status], + ) + return ( + + {detailPanelData} + {button && ( + + + + )} + + ) +} + +export default DetailPanel diff --git a/apps/web/screens/Grants/Grant/GrantSidebar/ExtraPanel.tsx b/apps/web/screens/Grants/Grant/GrantSidebar/ExtraPanel.tsx new file mode 100644 index 000000000000..3719b83d0ffd --- /dev/null +++ b/apps/web/screens/Grants/Grant/GrantSidebar/ExtraPanel.tsx @@ -0,0 +1,80 @@ +import React, { useMemo } from 'react' + +import { Box, Button, LinkV2, Stack } from '@island.is/island-ui/core' +import { isDefined } from '@island.is/shared/utils' +import { Grant } from '@island.is/web/graphql/schema' + +interface ExtraPanelProps { + grant: Grant +} + +const ExtraPanel: React.FC = ({ grant }) => { + const supportLinksPanelData = useMemo( + () => + grant.supportLinks + ?.map((link) => { + if (!link.url || !link.text || !link.id) { + return null + } + return ( + + + + ) + }) + .filter(isDefined) ?? [], + [grant.supportLinks], + ) + + const filesPanelData = useMemo( + () => + grant.files + ?.map((f, index) => { + if (!f.url) { + return null + } + return ( + + + + ) + }) + .concat(supportLinksPanelData) + .filter(isDefined) ?? [], + [grant.files, supportLinksPanelData], + ) + + return ( + + {filesPanelData} + + ) +} + +export default ExtraPanel diff --git a/apps/web/screens/Grants/Grant/GrantSidebar/GrantSidebar.tsx b/apps/web/screens/Grants/Grant/GrantSidebar/GrantSidebar.tsx new file mode 100644 index 000000000000..2579aab80c1a --- /dev/null +++ b/apps/web/screens/Grants/Grant/GrantSidebar/GrantSidebar.tsx @@ -0,0 +1,73 @@ +import { useIntl } from 'react-intl' + +import { Box, BoxProps, Button, LinkV2, Stack } from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' +import { InstitutionPanel } from '@island.is/web/components' +import { Grant } from '@island.is/web/graphql/schema' + +import { m } from '../../messages' +import DetailPanel from './DetailPanel' +import ExtraPanel from './ExtraPanel' +import * as styles from '../Grant.css' + +interface Props { + grant: Grant + locale: Locale +} + +export const generateSidebarPanel = ( + data: Array, + background: BoxProps['background'], +) => { + if (!data.length) { + return undefined + } + return ( + + {data} + + ) +} + +export const GrantSidebar = ({ grant, locale }: Props) => { + const { formatMessage } = useIntl() + + const goBackToDashboard = () => { + return ( + + + + + + ) + } + return ( + <> + {goBackToDashboard()} + + + + + + + ) +} diff --git a/apps/web/screens/Grants/Grant/GrantSidebar/PanelLine.tsx b/apps/web/screens/Grants/Grant/GrantSidebar/PanelLine.tsx new file mode 100644 index 000000000000..ad3aaba858fc --- /dev/null +++ b/apps/web/screens/Grants/Grant/GrantSidebar/PanelLine.tsx @@ -0,0 +1,15 @@ +import { Box, Text } from '@island.is/island-ui/core' + +export const generateLine = (heading: string, content?: React.ReactNode) => { + if (!content) { + return null + } + return ( + + + {heading} + + {content} + + ) +} diff --git a/apps/web/screens/Grants/messages.ts b/apps/web/screens/Grants/messages.ts index d6a5ce251693..ff6e5c75d6b2 100644 --- a/apps/web/screens/Grants/messages.ts +++ b/apps/web/screens/Grants/messages.ts @@ -6,6 +6,10 @@ export const m = { id: 'web.grants:general.seeMore', defaultMessage: 'Skoða nánar', }, + goBack: { + id: 'web.grants:general.goBack', + defaultMessage: 'Til baka', + }, displayGrid: { id: 'web.grants:general.displayGrid', defaultMessage: 'Sýna sem spjöld', @@ -82,7 +86,7 @@ export const m = { }, applicationWasOpenToAndWith: { id: 'web.grants:search.applicationWasOpenToAndWith', - defaultMessage: 'Frestur var til og með {arg}', + defaultMessage: 'Frestur var til {arg}', }, applicationAlwaysOpen: { id: 'web.grants:search.applicationAlwaysOpen', diff --git a/apps/web/screens/Grants/utils.ts b/apps/web/screens/Grants/utils.ts index dbc65ab84978..97eb9bf5d7a5 100644 --- a/apps/web/screens/Grants/utils.ts +++ b/apps/web/screens/Grants/utils.ts @@ -17,7 +17,7 @@ interface Status { const formatDate = ( date: Date, locale: Locale, - stringFormat = 'dd.MMMM yyyy', + stringFormat = 'dd. MMMM yyyy', ): string | undefined => { try { return format(date, stringFormat, { @@ -100,7 +100,7 @@ export const parseStatus = ( } case GrantStatus.Open: { const date = grant.dateTo - ? formatDate(new Date(grant.dateTo), locale, 'dd.MMMM.') + ? formatDate(new Date(grant.dateTo), locale, 'dd. MMMM.') : undefined return { applicationStatus: 'open', diff --git a/apps/web/screens/queries/Organization.tsx b/apps/web/screens/queries/Organization.tsx index 8a1c92b5a600..5a04733342db 100644 --- a/apps/web/screens/queries/Organization.tsx +++ b/apps/web/screens/queries/Organization.tsx @@ -127,6 +127,7 @@ export const GET_ORGANIZATION_PAGE_QUERY = gql` slug title description + canBeFoundInSearchResults topLevelNavigation { links { label diff --git a/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts b/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts index 9c4bdfc139ed..61395b471319 100644 --- a/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts +++ b/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts @@ -28,14 +28,20 @@ export class HealthDirectorateService { if (data === null) { return null } + const hasExceptionComment: boolean = + data.exceptionComment !== undefined && data.exceptionComment.length > 0 + const hasExceptions: boolean = + data.exceptions !== undefined && data.exceptions.length > 0 const donorStatus: Donor = { - isDonor: data?.isDonor ?? true, + isDonor: data.isDonor, limitations: { hasLimitations: - ((data?.exceptions?.length ?? 0) > 0 && data?.isDonor) ?? false, - limitedOrgansList: data?.exceptions, - comment: data?.exceptionComment, + ((hasExceptionComment || hasExceptions) && data.isDonor) ?? false, + limitedOrgansList: data.exceptions, + comment: data.exceptionComment, }, + isMinor: data.isMinor ?? false, + isTemporaryResident: data.isTemporaryResident ?? false, } return donorStatus } @@ -62,11 +68,15 @@ export class HealthDirectorateService { input: DonorInput, locale: Locale, ): Promise { + const filteredList = + input.organLimitations?.filter((item) => item !== 'other') ?? [] + return await this.organDonationApi.updateOrganDonation( auth, { isDonor: input.isDonor, - exceptions: input.organLimitations ?? [], + exceptions: filteredList, + exceptionComment: input.comment, }, locale === 'is' ? organLocale.Is : organLocale.En, ) diff --git a/libs/api/domains/health-directorate/src/lib/models/organ-donation.model.ts b/libs/api/domains/health-directorate/src/lib/models/organ-donation.model.ts index 8893f2149324..824124479661 100644 --- a/libs/api/domains/health-directorate/src/lib/models/organ-donation.model.ts +++ b/libs/api/domains/health-directorate/src/lib/models/organ-donation.model.ts @@ -33,6 +33,12 @@ export class Donor { @Field(() => Limitations, { nullable: true }) limitations?: Limitations + + @Field(() => Boolean, { defaultValue: false }) + isMinor!: boolean + + @Field(() => Boolean, { defaultValue: false }) + isTemporaryResident!: boolean } @ObjectType('HealthDirectorateOrganDonation') @@ -54,4 +60,7 @@ export class DonorInput { @Field(() => [String], { nullable: true }) organLimitations?: string[] + + @Field({ nullable: true }) + comment?: string } diff --git a/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.input.ts b/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.input.ts index 868f78ae3130..0e17f4f59ecd 100644 --- a/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.input.ts +++ b/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.input.ts @@ -1,5 +1,5 @@ import { AdvertSignatureTypeEnum } from '@island.is/clients/official-journal-of-iceland' -import { InputType, Field, registerEnumType } from '@nestjs/graphql' +import { InputType, Field, registerEnumType, Int } from '@nestjs/graphql' registerEnumType(AdvertSignatureTypeEnum, { name: 'OfficialJournalOfIcelandAdvertSignatureType', @@ -10,10 +10,10 @@ export class AdvertsInput { @Field(() => String, { nullable: true }) search?: string - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) page?: number - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) pageSize?: number @Field(() => [String], { nullable: true }) @@ -43,10 +43,10 @@ export class TypeQueryParams { @Field(() => String, { nullable: true }) department?: string - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) page?: number - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) pageSize?: number } @@ -61,10 +61,10 @@ export class QueryParams { @Field(() => String, { nullable: true }) search?: string - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) page?: number - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) pageSize?: number } @@ -134,3 +134,15 @@ export class SubmitApplicationInput { @Field(() => AdvertSignature) signature!: AdvertSignature } + +@InputType('OfficialJournalOfIcelandMainTypesInput') +export class MainTypesQueryParams { + @Field(() => String, { nullable: true }) + department?: string + + @Field(() => Int, { nullable: true }) + page?: number + + @Field(() => Int, { nullable: true }) + pageSize?: number +} diff --git a/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.model.ts b/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.model.ts index 30d22c9840f5..70a9135f7d45 100644 --- a/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.model.ts +++ b/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.model.ts @@ -136,3 +136,21 @@ export class Advert { @Field(() => AdvertDocument) document!: AdvertDocument } + +@ObjectType('OfficialJournalOfIcelandAdvertsMainType') +export class AdvertMainType { + @Field() + id!: string + + @Field() + title!: string + + @Field() + slug!: string + + @Field(() => AdvertEntity) + department!: AdvertEntity + + @Field(() => [AdvertType]) + types!: AdvertType[] +} diff --git a/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.response.ts b/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.response.ts index 633157789400..baa06f7a4f6f 100644 --- a/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.response.ts +++ b/libs/api/domains/official-journal-of-iceland/src/lib/models/advert.response.ts @@ -5,6 +5,7 @@ import { AdvertEntity, AdvertMainCategory, AdvertType, + AdvertMainType, } from './advert.model' import { AdvertPaging } from './advert-paging.model' @@ -79,3 +80,12 @@ export class AdvertResponse { @Field(() => Advert) advert?: Advert } + +@ObjectType('OfficialJournalOfIcelandMainTypesResponse') +export class MainTypesResponse { + @Field(() => [AdvertMainType]) + mainTypes!: AdvertMainType[] + + @Field(() => AdvertPaging) + paging!: AdvertPaging +} diff --git a/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.resolver.ts b/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.resolver.ts index cde591d90f87..1fddbf0d108a 100644 --- a/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.resolver.ts +++ b/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.resolver.ts @@ -8,6 +8,7 @@ import { AdvertsInput, QueryParams, TypeQueryParams, + MainTypesQueryParams, } from './models/advert.input' import { AdvertCategoryResponse, @@ -19,6 +20,7 @@ import { AdvertsResponse, AdvertTypeResponse, AdvertTypesResponse, + MainTypesResponse, } from './models/advert.response' import { Features } from '@island.is/feature-flags' import { FeatureFlag } from '@island.is/nest/feature-flags' @@ -72,6 +74,13 @@ export class OfficialJournalOfIcelandResolver { return this.ojoiService.getAdvertTypes(params) } + @Query(() => MainTypesResponse, { + name: 'officialJournalOfIcelandMainTypes', + }) + getAdvertMainTypes(@Args('params') params: MainTypesQueryParams) { + return this.ojoiService.getMainTypes(params) + } + @Query(() => AdvertMainCategoriesResponse, { name: 'officialJournalOfIcelandMainCategories', }) diff --git a/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.service.ts b/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.service.ts index 954a7ab1d8ad..39d9c4c2507d 100644 --- a/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.service.ts +++ b/libs/api/domains/official-journal-of-iceland/src/lib/officialJournalOfIceland.service.ts @@ -8,6 +8,7 @@ import { AdvertSingleParams, QueryParams, TypeQueryParams, + MainTypesQueryParams, } from './models/advert.input' import { AdvertCategoryResponse, @@ -17,6 +18,7 @@ import { AdvertResponse, AdvertsResponse, AdvertTypesResponse, + MainTypesResponse, } from './models/advert.response' import { CasesInProgressResponse } from './models/cases.response' @@ -54,6 +56,10 @@ export class OfficialJournalOfIcelandService { return await this.ojoiService.getAdvertTypes(params) } + async getMainTypes(params: MainTypesQueryParams): Promise { + return await this.ojoiService.getAdvertMainTypes(params) + } + async getInstitutions( params: QueryParams, ): Promise { diff --git a/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts b/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts index b4e0e2ab0f8e..87fbccb3d32e 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts @@ -157,6 +157,7 @@ export const expandAnswers = ( deceasedShare: gun.deceasedShare ?? '', deceasedShareEnabled: gun.deceasedShareEnabled ?? [], deceasedShareAmount: gun.deceasedShareAmount ?? 0, + enabled: gun.enabled ?? true, } }), total: answers.assets.guns?.total ?? 0, @@ -199,6 +200,7 @@ export const expandAnswers = ( deceasedShare: realEstate.deceasedShare ?? '0', deceasedShareEnabled: realEstate.deceasedShareEnabled ?? [], deceasedShareAmount: realEstate.deceasedShareAmount ?? 0, + enabled: realEstate.enabled ?? true, } }), total: answers.assets.realEstate?.total ?? 0, @@ -227,6 +229,7 @@ export const expandAnswers = ( deceasedShare: vehicle.deceasedShare ?? '0', deceasedShareEnabled: vehicle.deceasedShareEnabled ?? [], deceasedShareAmount: vehicle?.deceasedShareAmount ?? 0, + enabled: vehicle.enabled ?? true, } }), total: answers.assets.vehicles?.total ?? 0, diff --git a/libs/application/templates/accident-notification/src/utils/miscUtils.ts b/libs/application/templates/accident-notification/src/utils/miscUtils.ts index bee5dd87bf40..5ae53a9533cc 100644 --- a/libs/application/templates/accident-notification/src/utils/miscUtils.ts +++ b/libs/application/templates/accident-notification/src/utils/miscUtils.ts @@ -1,4 +1,3 @@ -import { AccidentNotificationConfirmation } from '@island.is/api/schema' import { getValueViaPath } from '@island.is/application/core' import { FormValue, YES } from '@island.is/application/types' import { isReportingOnBehalfOfEmployee as isReportingOnBehalfOfEmployeeOrginal } from './reportingUtils' @@ -28,10 +27,14 @@ export const formatPhonenumber = (value: string) => { } export const hasReceivedConfirmation = (answers: FormValue) => { + // The fetched value is actually typed as AccidentNotificationConfirmation, but importing that type breaks when codegen is run after cleanup const accidentConfirmations = getValueViaPath( answers, 'accidentStatus.receivedConfirmations', - ) as AccidentNotificationConfirmation + ) as { + InjuredOrRepresentativeParty: boolean | undefined + CompanyParty: boolean | undefined + } // if juridical person then the injured or the power of attorney holder has to confirm if (isReportingOnBehalfOfEmployee(answers)) { diff --git a/libs/application/templates/inheritance-report/src/forms/prerequisites.ts b/libs/application/templates/inheritance-report/src/forms/prerequisites.ts index 74b17ab2358b..5296679ded4b 100644 --- a/libs/application/templates/inheritance-report/src/forms/prerequisites.ts +++ b/libs/application/templates/inheritance-report/src/forms/prerequisites.ts @@ -38,8 +38,6 @@ export const getForm = ({ value: ESTATE_INHERITANCE, label: m.preDataCollectionApplicationForDefault, disabled: !allowEstateApplication, - //TODO: remove tooltip when this application is ready to go live - tooltip: m.preDataCollectionApplicationForDefaultTooltip, }, { value: PREPAID_INHERITANCE, diff --git a/libs/application/templates/inheritance-report/src/forms/sections/assets.ts b/libs/application/templates/inheritance-report/src/forms/sections/assets.ts index a8b9ab5fe2fd..b2c3d8b511ab 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/assets.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/assets.ts @@ -345,7 +345,7 @@ export const assets = buildSection({ [ESTATE_INHERITANCE]: m.bankAccountCapital, [PREPAID_INHERITANCE]: m.bankAccountCapitalPrePaid, }, - id: 'propertyValuation', + id: 'amount', required: true, currency: true, }, @@ -376,7 +376,7 @@ export const assets = buildSection({ fromExternalData: 'bankAccounts', skipPushRight: true, repeaterButtonText: m.bankAccountRepeaterButton, - sumField: 'propertyValuation', + sumField: 'amount', sumField2: 'exchangeRateOrInterest', }, ), @@ -494,13 +494,19 @@ export const assets = buildSection({ format: '######-####', }, { - title: m.stocksFaceValue, + title: { + [ESTATE_INHERITANCE]: m.stocksFaceValue, + [PREPAID_INHERITANCE]: m.stocksFaceValuePrePaid, + }, id: 'amount', currency: true, required: true, }, { - title: m.stocksRateOfChange, + title: { + [ESTATE_INHERITANCE]: m.stocksRateOfChange, + [PREPAID_INHERITANCE]: m.stocksRateOfChangePrePaid, + }, id: 'exchangeRateOrInterest', type: 'currency', required: true, diff --git a/libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts b/libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts index ed46966a1955..8651c95870f1 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts @@ -87,6 +87,9 @@ export const debtsAndFuneralCost = buildSection({ { label: DebtTypes.OtherDebts, }, + { + label: DebtTypes.InsuranceInstitute, + }, ], }, ), diff --git a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts index dc53eb07648d..89606f99a45b 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/applicant.ts @@ -20,7 +20,7 @@ export const prePaidApplicant = buildSection({ children: [ buildNationalIdWithNameField({ id: 'prePaidApplicant', - title: m.name, + title: '', width: 'full', required: true, }), diff --git a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts index a411304e2a32..593efcd2b8ae 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts @@ -26,7 +26,7 @@ export const inheritanceExecutor = buildSection({ }), buildNationalIdWithNameField({ id: 'executors.executor', - title: m.name, + title: '', required: true, }), buildTextField({ @@ -73,7 +73,7 @@ export const inheritanceExecutor = buildSection({ }), buildNationalIdWithNameField({ id: 'executors.spouse', - title: m.name, + title: '', required: true, condition: (answers) => !!((answers.executors as any)?.includeSpouse as Array) diff --git a/libs/application/templates/inheritance-report/src/lib/dataSchema.ts b/libs/application/templates/inheritance-report/src/lib/dataSchema.ts index 3e437681836f..0e77b8b62b09 100644 --- a/libs/application/templates/inheritance-report/src/lib/dataSchema.ts +++ b/libs/application/templates/inheritance-report/src/lib/dataSchema.ts @@ -55,6 +55,7 @@ const assetSchema = ({ withShare }: { withShare?: boolean } = {}) => .refine((v) => (withShare ? validateAssetNumber(v) : true)), description: z.string(), propertyValuation: z.string(), + enabled: z.boolean(), ...(withShare ? { share: z.string() } : {}), ...deceasedShare, }) diff --git a/libs/application/templates/inheritance-report/src/lib/messages.ts b/libs/application/templates/inheritance-report/src/lib/messages.ts index 84f67d71a06a..e0b816cab74e 100644 --- a/libs/application/templates/inheritance-report/src/lib/messages.ts +++ b/libs/application/templates/inheritance-report/src/lib/messages.ts @@ -708,11 +708,21 @@ export const m = defineMessages({ }, stocksFaceValue: { id: 'ir.application:stocksFaceValue', + defaultMessage: 'Nafnverð á dánardegi', + description: '', + }, + stocksFaceValuePrePaid: { + id: 'ir.application:stocksFaceValuePrePaid', defaultMessage: 'Nafnverð', description: '', }, stocksRateOfChange: { id: 'ir.application:stocksRateOfChange', + defaultMessage: 'Gengi á dánardegi', + description: '', + }, + stocksRateOfChangePrePaid: { + id: 'ir.application:stocksRateOfChangePrePaid', defaultMessage: 'Gengi', description: '', }, diff --git a/libs/application/templates/inheritance-report/src/types.ts b/libs/application/templates/inheritance-report/src/types.ts index 5b4b730a8fbb..c23a10263a35 100644 --- a/libs/application/templates/inheritance-report/src/types.ts +++ b/libs/application/templates/inheritance-report/src/types.ts @@ -305,4 +305,5 @@ export enum DebtTypes { PropertyFees = 'Fasteignagjöld', OtherDebts = 'Aðrar skuldir', PublicCharges = 'Opinber gjöld', + InsuranceInstitute = 'Tryggingarstofnun ríkisins', } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx b/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx index f8cb1367ec92..0b91a291d5e7 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx @@ -3,7 +3,7 @@ import { Editor, EditorFileUploader } from '@island.is/regulations-tools/Editor' import { useEffect, useRef, useState } from 'react' import { Controller } from 'react-hook-form' import { classes, editorWrapper, errorStyle } from './HTMLEditor.css' -import { Box, Text } from '@island.is/island-ui/core' +import { Box, Stack, Text } from '@island.is/island-ui/core' type Props = { title?: string name: string @@ -50,12 +50,8 @@ export const HTMLEditor = ({ defaultValue={initialValue} render={({ field: { onChange: updateFormValue, value } }) => { return ( - <> - {title && ( - - {title} - - )} + + {title && {title}} {error &&
{error}
} - +
) }} /> diff --git a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx index 00eac25ff080..799ed40704d8 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx @@ -4,7 +4,12 @@ import { useApplication } from '../../hooks/useUpdateApplication' import { OJOIApplication } from '../../lib/types' import { useFormContext } from 'react-hook-form' import set from 'lodash/set' -import { Select, SkeletonLoader } from '@island.is/island-ui/core' +import { + Box, + Select, + SkeletonLoader, + useBreakpoint, +} from '@island.is/island-ui/core' import { OJOI_INPUT_HEIGHT } from '../../lib/constants' import { isBaseEntity } from '../../lib/utils' import { getValueViaPath } from '@island.is/application/core' @@ -23,6 +28,7 @@ type Props = { loading?: boolean applicationId: string disabled?: boolean + width?: 'full' | 'half' onBeforeChange?: (answers: OJOIApplication['answers'], value: T) => void onChange?: (value: T) => void } @@ -36,6 +42,7 @@ export const OJOISelectController = ({ loading, applicationId, disabled, + width = 'full', onBeforeChange, onChange, }: Props) => { @@ -46,6 +53,9 @@ export const OJOISelectController = ({ const { setValue } = useFormContext() + const { xs, sm, md } = useBreakpoint() + const isSmOrSmaller = xs || (sm && !md) + const placeholderText = typeof placeholder === 'string' ? placeholder : f(placeholder) @@ -68,35 +78,33 @@ export const OJOISelectController = ({ return opt.value.id === defaultVal.id } - return false + return undefined }) - if (loading) { - return ( - - ) - } - return ( - { + 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 0e81b3062f40..fcaa5cfdb6fc 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,8 +1,7 @@ import { InputFields, OJOIFieldBaseProps } from '../lib/types' -import { Box } from '@island.is/island-ui/core' +import { Stack } from '@island.is/island-ui/core' import { FormGroup } from '../components/form/FormGroup' import { advert } from '../lib/messages' -import * as styles from './Advert.css' import { useDepartments } from '../hooks/useDepartments' import { OJOISelectController } from '../components/input/OJOISelectController' import { useTypes } from '../hooks/useTypes' @@ -12,25 +11,23 @@ import { useFormContext } from 'react-hook-form' import { useApplication } from '../hooks/useUpdateApplication' import set from 'lodash/set' import { HTMLEditor } from '../components/htmlEditor/HTMLEditor' -import { getAdvertMarkup } from '../lib/utils' +import { cleanTypename, getAdvertMarkup } from '../lib/utils' +import { DEPARTMENT_A } from '../lib/constants' export const Advert = ({ application }: OJOIFieldBaseProps) => { const { setValue } = useFormContext() const { application: currentApplication } = useApplication({ applicationId: application.id, }) + const { departments, loading: loadingDepartments } = useDepartments() - const { - getLazyTypes, - types, - loading: loadingTypes, - } = useTypes({ - initalDepartmentId: application.answers?.advert?.department?.id, - }) - const titlePreview = getAdvertMarkup({ - type: currentApplication.answers.advert?.type?.title, - title: currentApplication.answers.advert?.title, + const defaultDepartment = + application.answers?.advert?.department?.id || DEPARTMENT_A + + const { getLazyMainTypes, mainTypes, mainTypeLoading } = useTypes({ + initalDepartmentId: defaultDepartment, + pageSize: 300, }) const departmentOptions = departments?.map((d) => ({ @@ -42,20 +39,28 @@ export const Advert = ({ application }: OJOIFieldBaseProps) => { }, })) - const typeOptions = types?.map((d) => ({ + const mainTypeOptions = mainTypes?.map((d) => ({ label: d.title, - value: { - id: d.id, - title: d.title, - slug: d.slug, - }, + value: d, })) + const currentTypes = + currentApplication?.answers?.advert?.mainType?.types?.map((d) => ({ + label: d.title, + value: d, + })) ?? [] + + const titlePreview = getAdvertMarkup({ + type: currentApplication.answers.advert?.type?.title, + title: currentApplication.answers.advert?.title, + }) + return ( - <> + - + { set(answers, InputFields.advert.type, null) }} onChange={(value) => - getLazyTypes({ + getLazyMainTypes({ variables: { params: { department: value.id, @@ -78,19 +83,36 @@ export const Advert = ({ application }: OJOIFieldBaseProps) => { }) } /> - - + { + if (value.types.length === 1) { + const cleaned = cleanTypename(value.types[0]) + set(answers, InputFields.advert.type, cleaned) + } else { + set(answers, InputFields.advert.type, null) + } + }} /> - - + + {currentTypes.length > 1 && ( + + )} + { textarea={true} maxLength={600} /> - - + - + - + { applicationId={application.id} disabled={true} /> - - + { // 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 8327cefa5e93..5020d1b8fbe7 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 @@ -26,6 +26,7 @@ import debounce from 'lodash/debounce' import { InputFields } from '../lib/types' import { useFormContext } from 'react-hook-form' import { OfficialJournalOfIcelandAdvert } from '@island.is/api/schema' +import { cleanTypename } from '../lib/utils' type Props = { applicationId: string visible: boolean @@ -75,20 +76,12 @@ export const AdvertModal = ({ return } - 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) + const department = cleanTypename(advert.department) + const type = cleanTypename(advert.type) - const categories = advert.categories.map((category) => clean(category)) + const categories = advert.categories.map((category) => + cleanTypename(category), + ) setValue(InputFields.advert.department, department) setValue(InputFields.advert.type, type) diff --git a/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts b/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts index ebe437b894ad..cfe4f70ab68b 100644 --- a/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts +++ b/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts @@ -117,6 +117,38 @@ export const ADVERT_QUERY = gql` } ` +export const MAIN_TYPES_QUERY = gql` + query AdvertMainTypes($params: OfficialJournalOfIcelandMainTypesInput!) { + officialJournalOfIcelandMainTypes(params: $params) { + mainTypes { + id + title + slug + department { + id + title + slug + } + types { + id + title + slug + } + } + paging { + page + pageSize + totalPages + totalItems + hasNextPage + hasPreviousPage + nextPage + previousPage + } + } + } +` + export const TYPES_QUERY = gql` query AdvertTypes($params: OfficialJournalOfIcelandTypesInput!) { officialJournalOfIcelandTypes(params: $params) { 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 8cd4ff5678ed..7ff98ea1949f 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,7 +1,10 @@ import { useLazyQuery, useQuery } from '@apollo/client' -import { OfficialJournalOfIcelandAdvertsTypesResponse } from '@island.is/api/schema' +import { + OfficialJournalOfIcelandAdvertsTypesResponse, + OfficialJournalOfIcelandMainTypesResponse, +} from '@island.is/api/schema' -import { TYPES_QUERY } from '../graphql/queries' +import { MAIN_TYPES_QUERY, TYPES_QUERY } from '../graphql/queries' type UseTypesParams = { initalDepartmentId?: string @@ -14,6 +17,10 @@ type TypesResponse = { officialJournalOfIcelandTypes: OfficialJournalOfIcelandAdvertsTypesResponse } +type MainTypesResponse = { + officialJournalOfIcelandMainTypes: OfficialJournalOfIcelandMainTypesResponse +} + type TypesVariables = { params: { department?: string @@ -50,6 +57,16 @@ export const useTypes = ({ }, ) + const { + data: mainTypesData, + loading: mainTypeLoading, + error: mainTypeError, + } = useQuery(MAIN_TYPES_QUERY, { + variables: { + params: params, + }, + }) + const [ getLazyTypes, { data: lazyTypes, loading: lazyTypesLoading, error: lazyTypesError }, @@ -57,11 +74,33 @@ export const useTypes = ({ fetchPolicy: 'network-only', }) + const [ + getLazyMainTypes, + { + data: lazyMainTypes, + loading: lazyMainTypesLoading, + error: lazyMainTypesError, + }, + ] = useLazyQuery(MAIN_TYPES_QUERY, { + fetchPolicy: 'network-only', + }) + const currentTypes = lazyTypes ? lazyTypes.officialJournalOfIcelandTypes.types : data?.officialJournalOfIcelandTypes.types + const currentMainTypes = lazyMainTypes + ? lazyMainTypes.officialJournalOfIcelandMainTypes.mainTypes + : mainTypesData?.officialJournalOfIcelandMainTypes.mainTypes + return { + mainTypes: currentMainTypes, + mainTypeLoading, + mainTypeError, + lazyMainTypesLoading, + lazyMainTypesError, + getLazyMainTypes, + lazyMainTypes: lazyMainTypes?.officialJournalOfIcelandMainTypes.mainTypes, lazyTypes: lazyTypes?.officialJournalOfIcelandTypes.types, lazyTypesLoading, lazyTypesError, diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts b/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts index e05b15dcaf66..ed4ea3efc463 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts @@ -14,6 +14,10 @@ export enum AnswerOption { NO = 'no', } +export const DEPARTMENT_A = 'a-deild' +export const DEPARTMENT_B = 'b-deild' +export const DEPARTMENT_C = 'c-deild' + export enum ApplicationAttachmentType { ORIGINAL = 'frumrit', ADDITIONS = 'fylgiskjol', 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 046cbeed2664..8dcfe02c7bbd 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 @@ -69,6 +69,9 @@ const advertSchema = z .object({ department: baseEntitySchema.optional(), type: baseEntitySchema.optional().nullable(), + mainType: baseEntitySchema + .extend({ types: z.array(baseEntitySchema).optional() }) + .optional(), title: z.string().optional(), html: z.string().optional(), requestedDate: z.string().optional(), diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts index 681773cf8393..0bde1ec2be09 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts @@ -63,15 +63,27 @@ export const advert = { description: 'Placeholder for the department input', }, }), + mainType: defineMessages({ + label: { + id: 'ojoi.application:advert.inputs.mainType.label', + defaultMessage: 'Tegund birtingar', + description: 'Label for the main type input', + }, + placeholder: { + id: 'ojoi.application:advert.inputs.mainType.placeholder', + defaultMessage: 'Veldu tegund birtingar', + description: 'Placeholder for the main type input', + }, + }), type: defineMessages({ label: { id: 'ojoi.application:advert.inputs.type.label', - defaultMessage: 'Tegund birtingar', + defaultMessage: 'Undirtegund birtingar', description: 'Label for the type input', }, placeholder: { id: 'ojoi.application:advert.inputs.type.placeholder', - defaultMessage: 'Veldu tegund birtingar', + defaultMessage: 'Veldu undirtegund birtingar', description: 'Placeholder for the type input', }, }), 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 1529e2d56442..032d13c63692 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 @@ -12,6 +12,7 @@ export const InputFields = { }, [Routes.ADVERT]: { department: 'advert.department', + mainType: 'advert.mainType', type: 'advert.type', title: 'advert.title', html: 'advert.html', 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 15699f900241..fdfaad3ab0d9 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 @@ -388,3 +388,13 @@ export const convertNumberToRoman = (num: number) => { const roman = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'] return roman[num - 1] } + +export const cleanTypename = (obj: { + __typename?: string + id: string + title: string + slug: string +}) => { + const { __typename: _, ...rest } = obj + return rest +} diff --git a/libs/application/templates/official-journal-of-iceland/src/screens/InvolvedPartyScreen.tsx b/libs/application/templates/official-journal-of-iceland/src/screens/InvolvedPartyScreen.tsx index 7ed23a605848..b35efca32233 100644 --- a/libs/application/templates/official-journal-of-iceland/src/screens/InvolvedPartyScreen.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/screens/InvolvedPartyScreen.tsx @@ -90,21 +90,20 @@ export const InvolvedPartyScreen = ({ /> )} - - { - setSubmitButtonDisabled && setSubmitButtonDisabled(false) - }} - /> - + { + setSubmitButtonDisabled && setSubmitButtonDisabled(false) + }} + /> ) diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts index bb74ed0a4d6e..701fa0785810 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 @@ -27,8 +27,10 @@ import { DelegationDelegationType } from './models/delegation-delegation-type.mo import { DelegationScope } from './models/delegation-scope.model' import { DelegationTypeModel } from './models/delegation-type.model' import { Delegation } from './models/delegation.model' +import { ApiScopeInfo } from './delegations-incoming.service' import type { User } from '@island.is/auth-nest-tools' +import filterByCustomScopeRule from './utils/filterByScopeCustomScopeRule' @Injectable() export class DelegationScopeService { @@ -222,7 +224,18 @@ export class DelegationScopeService { }, ], }) - .then((apiScopes) => apiScopes.map((apiScope) => apiScope.name)) + .then((apiScopes) => + apiScopes + .filter((scope) => + // Remove scopes that are not allowed for the delegation type + filterByCustomScopeRule( + scope, + [AuthDelegationType.GeneralMandate], + this.delegationConfig.customScopeRules, + ), + ) + .map((apiScope) => apiScope.name), + ) } private async findAllNationalRegistryScopes(): Promise { 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 e11e509095bd..2eac4bf93fe7 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 @@ -26,6 +26,7 @@ import { Delegation } from './models/delegation.model' import { NationalRegistryV3FeatureService } from './national-registry-v3-feature.service' import { DelegationValidity } from './types/delegationValidity' import { getScopeValidityWhereClause } from './utils/scopes' +import filterByCustomScopeRule from './utils/filterByScopeCustomScopeRule' type FindAllValidIncomingOptions = { nationalId: string @@ -182,20 +183,6 @@ export class DelegationsIncomingCustomService { }) } - private filterByCustomScopeRule(scope: ApiScopeInfo) { - const foundCSR = this.delegationConfig.customScopeRules.find( - (csr) => csr.scopeName === scope.name, - ) - - if (!foundCSR) { - return true - } - - return foundCSR.onlyForDelegationType.includes( - AuthDelegationType.GeneralMandate, - ) - } - /** * Finds all companies that have a general mandate for the user. * @param user @@ -233,7 +220,11 @@ export class DelegationsIncomingCustomService { const customApiScopes = clientAllowedApiScopes.filter( (s) => !s.isAccessControlled && - this.filterByCustomScopeRule(s) && + filterByCustomScopeRule( + s, + [AuthDelegationType.GeneralMandate], + this.delegationConfig.customScopeRules, + ) && s.supportedDelegationTypes?.some((dt) => supportedDelegationTypes.includes( dt.delegationType as AuthDelegationType, diff --git a/libs/auth-api-lib/src/lib/delegations/utils/filterByScopeCustomScopeRule.ts b/libs/auth-api-lib/src/lib/delegations/utils/filterByScopeCustomScopeRule.ts new file mode 100644 index 000000000000..87fbd1d7adff --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/utils/filterByScopeCustomScopeRule.ts @@ -0,0 +1,21 @@ +import { AuthDelegationType } from '@island.is/shared/types' +import { ApiScopeInfo } from '../delegations-incoming.service' + +export default function filterByCustomScopeRule( + scope: ApiScopeInfo, + filterOutForDelegationType: AuthDelegationType[], + customScopeRules: { + scopeName: string + onlyForDelegationType: string[] + }[], +): boolean { + const foundCSR = customScopeRules.find((csr) => csr.scopeName === scope.name) + + if (!foundCSR) { + return true + } + + return foundCSR.onlyForDelegationType.some((type) => + filterOutForDelegationType.includes(type as AuthDelegationType), + ) +} diff --git a/libs/auth-api-lib/src/lib/resources/delegation-resources.service.ts b/libs/auth-api-lib/src/lib/resources/delegation-resources.service.ts index 3f5b80d434dd..7de2870f58bd 100644 --- a/libs/auth-api-lib/src/lib/resources/delegation-resources.service.ts +++ b/libs/auth-api-lib/src/lib/resources/delegation-resources.service.ts @@ -25,6 +25,7 @@ import { mapToScopeTree } from './utils/scope-tree.mapper' import type { Attributes, WhereOptions } from 'sequelize' import type { ConfigType } from '@island.is/nest/config' +import { ApiScopeDelegationType } from './models/api-scope-delegation-type.model' type DelegationConfigType = ConfigType type ScopeRule = DelegationConfigType['customScopeRules'] extends Array< @@ -42,6 +43,8 @@ export class DelegationResourcesService { private domainModel: typeof Domain, @InjectModel(DelegationScope) private delegationScopeModel: typeof DelegationScope, + @InjectModel(ApiScopeDelegationType) + private apiScopeDelegationTypeModel: typeof ApiScopeDelegationType, private resourceTranslationService: ResourceTranslationService, @Inject(DelegationConfig.KEY) private delegationConfig: ConfigType, @@ -304,20 +307,37 @@ export class DelegationResourcesService { } private async delegationTypeFilter(user: User, prefix?: string) { - if (!user.delegationType || !user.actor) { + if (!user.delegationType) { return [] } // We currently only support access control for company (delegation) actors. // Actors for individuals should not have the scope required to reach this // point, but we assert it just to be safe. + // EDIT: This is no longer true, as we now support LegalRepresentative delegations for individuals. if (!isCompany(user.nationalId)) { - throw new ForbiddenException( - 'Actors for individuals should not be able to manage delegations.', - ) + if ( + !user.delegationType.includes(AuthDelegationType.LegalRepresentative) + ) { + throw new ForbiddenException( + 'Actors for individuals should not be able to manage delegations.', + ) + } } const delegationOr: Array> = [] + if (user.delegationType.includes(AuthDelegationType.LegalRepresentative)) { + const scopes = await this.apiScopeDelegationTypeModel.findAll({ + attributes: ['apiScopeName'], + where: { + delegationType: AuthDelegationType.LegalRepresentative, + }, + }) + + delegationOr.push({ + [col(prefix, 'name')]: scopes.map((scope) => scope.apiScopeName), + }) + } if (user.delegationType.includes(AuthDelegationType.ProcurationHolder)) { delegationOr.push({ [col(prefix, 'grantToProcuringHolders')]: true }) } diff --git a/libs/auth-api-lib/src/lib/resources/resources.module.ts b/libs/auth-api-lib/src/lib/resources/resources.module.ts index c7198b3ecef4..541380575a33 100644 --- a/libs/auth-api-lib/src/lib/resources/resources.module.ts +++ b/libs/auth-api-lib/src/lib/resources/resources.module.ts @@ -26,6 +26,7 @@ import { ResourceTranslationService } from './resource-translation.service' import { ResourcesService } from './resources.service' import { ScopeService } from './scope.service' import { TenantsService } from './tenants.service' +import { ApiScopeDelegationType } from './models/api-scope-delegation-type.model' @Module({ imports: [ @@ -39,6 +40,7 @@ import { TenantsService } from './tenants.service' ApiScopeUser, ApiScopeUserAccess, ApiResourceScope, + ApiScopeDelegationType, IdentityResourceUserClaim, ApiScopeUserClaim, ApiResourceUserClaim, diff --git a/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json b/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json index b83056a0484c..8612145527f7 100644 --- a/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json +++ b/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json @@ -1,18 +1,11 @@ { "openapi": "3.0.0", "paths": { - "/v1/me/organ-donor-status": { + "/v1/donation-exceptions": { "get": { - "operationId": "MeDonorStatusController_getOrganDonorStatus", - "description": "Get user's donation-exception donor status", + "operationId": "DonationExceptionController_getOrgans", + "description": "Gets a list of organs that can be omitted from an donation-exception donation", "parameters": [ - { - "name": "ip", - "required": false, - "in": "query", - "description": "The IP address of the user", - "schema": { "type": "string" } - }, { "name": "locale", "required": false, @@ -26,7 +19,10 @@ "description": "", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/OrganDonorDto" } + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/OrganDto" } + } } } }, @@ -63,37 +59,38 @@ } } }, - "tags": ["me/organ-donor-status"] - }, - "post": { - "operationId": "MeDonorStatusController_updateOrganDonorStatus", - "description": "Update user's donation-exception donor status", + "tags": ["donation-exceptions"] + } + }, + "/v1/me/organ-donor-status": { + "get": { + "operationId": "MeDonorStatusController_getOrganDonorStatus", + "description": "Get user's donation-exception donor status", "parameters": [ { - "name": "ip", + "name": "locale", "required": false, "in": "query", - "description": "The IP address of the user", - "schema": { "type": "string" } + "description": "The locale to use for the response", + "schema": { "$ref": "#/components/schemas/Locale" } }, { - "name": "locale", + "name": "ip", "required": false, "in": "query", - "description": "The locale to use for the response", - "schema": { "$ref": "#/components/schemas/Locale" } + "description": "The IP address of the user", + "schema": { "type": "string" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateOrganDonorDto" } - } - } - }, "responses": { - "200": { "description": "" }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/OrganDonorDto" } + } + } + }, "400": { "description": "", "content": { @@ -128,12 +125,10 @@ } }, "tags": ["me/organ-donor-status"] - } - }, - "/v1/donation-exceptions": { - "get": { - "operationId": "DonationExceptionController_getOrgans", - "description": "Gets a list of organs that can be omitted from an donation-exception donation", + }, + "post": { + "operationId": "MeDonorStatusController_updateOrganDonorStatus", + "description": "Update user's donation-exception donor status", "parameters": [ { "name": "locale", @@ -141,20 +136,25 @@ "in": "query", "description": "The locale to use for the response", "schema": { "$ref": "#/components/schemas/Locale" } + }, + { + "name": "ip", + "required": false, + "in": "query", + "description": "The IP address of the user", + "schema": { "type": "string" } } ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/OrganDto" } - } - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateOrganDonorDto" } } - }, + } + }, + "responses": { + "200": { "description": "" }, "400": { "description": "", "content": { @@ -188,7 +188,7 @@ } } }, - "tags": ["donation-exceptions"] + "tags": ["me/organ-donor-status"] } } }, @@ -226,24 +226,11 @@ }, "required": ["id", "name"] }, - "OrganDonorDto": { - "type": "object", - "properties": { - "isDonor": { "type": "boolean" }, - "exceptions": { - "type": "array", - "items": { "$ref": "#/components/schemas/OrganDto" } - }, - "exceptionComment": { "type": "string" }, - "registrationDate": { "format": "date-time", "type": "string" } - }, - "required": ["isDonor", "exceptions"] - }, "HttpProblemResponse": { "type": "object", "properties": { "type": { - "type": "object", + "type": "string", "description": "A URI reference that identifies the problem type" }, "title": { @@ -262,12 +249,27 @@ }, "required": ["type", "title"] }, + "OrganDonorDto": { + "type": "object", + "properties": { + "isDonor": { "type": "boolean" }, + "exceptions": { + "type": "array", + "items": { "$ref": "#/components/schemas/OrganDto" } + }, + "exceptionComment": { "type": "string" }, + "registrationDate": { "format": "date-time", "type": "string" }, + "isMinor": { "type": "boolean" }, + "isTemporaryResident": { "type": "boolean" } + }, + "required": ["isDonor", "exceptions"] + }, "UpdateOrganDonorDto": { "type": "object", "properties": { "isDonor": { "type": "boolean" }, "exceptions": { "type": "array", "items": { "type": "string" } }, - "exceptionComment": { "type": "object" } + "exceptionComment": { "type": "string" } }, "required": ["isDonor", "exceptions"] } diff --git a/libs/clients/official-journal-of-iceland/public/src/clientConfig.json b/libs/clients/official-journal-of-iceland/public/src/clientConfig.json index 092ccd7520e1..6206b173263a 100644 --- a/libs/clients/official-journal-of-iceland/public/src/clientConfig.json +++ b/libs/clients/official-journal-of-iceland/public/src/clientConfig.json @@ -169,15 +169,40 @@ } } }, - "/api/v1/types/{id}": { + "/api/v1/maincategories": { "get": { - "operationId": "getAdvertTypeById", + "operationId": "getMainCategories", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "search", + "description": "String to search for", + "required": false, + "in": "query", "schema": { "type": "string" } + }, + { + "name": "ids", + "required": false, + "in": "query", + "schema": { + "default": [], + "type": "array", + "items": { "type": "string" } + } + }, + { + "name": "page", + "description": "Page number to return.", + "required": false, + "in": "query", + "schema": { "type": "number" } + }, + { + "name": "pageSize", + "description": "Page size number to return.", + "required": false, + "in": "query", + "schema": { "type": "number" } } ], "responses": { @@ -186,7 +211,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetAdvertTypeResponse" + "$ref": "#/components/schemas/GetMainCategoriesResponse" } } } @@ -194,23 +219,26 @@ } } }, - "/api/v1/types": { + "/api/v1/categories": { "get": { - "operationId": "getAdvertTypes", + "operationId": "getCategories", "parameters": [ { - "name": "department", - "description": "Department slug to get categories for.", + "name": "search", + "description": "String to search for", "required": false, "in": "query", "schema": { "type": "string" } }, { - "name": "search", - "description": "String to search for in types.", + "name": "ids", "required": false, "in": "query", - "schema": { "type": "string" } + "schema": { + "default": [], + "type": "array", + "items": { "type": "string" } + } }, { "name": "page", @@ -221,7 +249,7 @@ }, { "name": "pageSize", - "description": "Number of items per page.", + "description": "Page size number to return.", "required": false, "in": "query", "schema": { "type": "number" } @@ -233,7 +261,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetAdvertTypesResponse" + "$ref": "#/components/schemas/GetCategoriesResponse" } } } @@ -241,9 +269,9 @@ } } }, - "/api/v1/maincategories": { + "/api/v1/institutions": { "get": { - "operationId": "getMainCategories", + "operationId": "getInstitutions", "parameters": [ { "name": "search", @@ -283,7 +311,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetMainCategoriesResponse" + "$ref": "#/components/schemas/GetInstitutionsResponse" } } } @@ -291,37 +319,36 @@ } } }, - "/api/v1/categories": { + "/api/v1/signatures": { "get": { - "operationId": "getCategories", + "operationId": "getSignatures", "parameters": [ { - "name": "search", - "description": "String to search for", + "description": "Search for a specific signature by id", "required": false, + "name": "id", "in": "query", "schema": { "type": "string" } }, { - "name": "ids", + "description": "Search for a specific signature by type", + "example": "Regular", "required": false, + "name": "type", "in": "query", - "schema": { - "default": [], - "type": "array", - "items": { "type": "string" } - } + "schema": { "type": "string" } }, { - "name": "page", - "description": "Page number to return.", + "description": "Search for a specific signature", + "example": "Dagur B. Eggertsson", "required": false, + "name": "search", "in": "query", - "schema": { "type": "number" } + "schema": { "type": "string" } }, { - "name": "pageSize", - "description": "Page size number to return.", + "name": "page", + "description": "Page number to return.", "required": false, "in": "query", "schema": { "type": "number" } @@ -333,7 +360,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetCategoriesResponse" + "$ref": "#/components/schemas/GetAdvertSignatureResponse" } } } @@ -341,9 +368,9 @@ } } }, - "/api/v1/institutions": { + "/api/v1/cases": { "get": { - "operationId": "getInstitutions", + "operationId": "getCasesInProgress", "parameters": [ { "name": "search", @@ -383,7 +410,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetInstitutionsResponse" + "$ref": "#/components/schemas/GetCasesInProgressReponse" } } } @@ -391,115 +418,384 @@ } } }, - "/api/v1/signatures": { + "/api/v1/error": { "get": { - "operationId": "getSignatures", + "operationId": "error", + "parameters": [], + "responses": { + "default": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ValidationResponse" } + } + } + } + } + } + }, + "/api/v1/advert-types/types": { + "get": { + "operationId": "getTypes", + "summary": "", "parameters": [ { - "description": "Search for a specific signature by id", "required": false, + "description": "Search by id", "name": "id", "in": "query", "schema": { "type": "string" } }, { - "description": "Search for a specific signature by type", - "example": "Regular", "required": false, - "name": "type", + "description": "Filter by unassigned", + "name": "unassigned", + "in": "query", + "schema": { "type": "boolean" } + }, + { + "required": false, + "description": "Search by main type id", + "name": "mainType", "in": "query", "schema": { "type": "string" } }, { - "description": "Search for a specific signature", - "example": "Dagur B. Eggertsson", "required": false, + "description": "Search by title", "name": "search", "in": "query", "schema": { "type": "string" } }, { + "required": false, + "description": "Search by slug", + "name": "slug", + "in": "query", + "schema": { "type": "string" } + }, + { + "required": false, + "description": "Search by department slug, title or id", + "name": "department", + "in": "query", + "schema": { "type": "string" } + }, + { + "required": false, + "description": "The page number", "name": "page", - "description": "Page number to return.", + "in": "query", + "schema": { "type": "number" } + }, + { "required": false, + "description": "The page size", + "name": "pageSize", "in": "query", "schema": { "type": "number" } } ], "responses": { - "default": { + "200": { "description": "", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/GetAdvertSignatureResponse" - } + "schema": { "$ref": "#/components/schemas/GetAdvertTypes" } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } } } } } } }, - "/api/v1/cases": { + "/api/v1/advert-types/main-types": { "get": { - "operationId": "getCasesInProgress", + "operationId": "getMainTypes", + "summary": "", "parameters": [ { + "required": false, + "description": "Search by id", + "name": "id", + "in": "query", + "schema": { "type": "string" } + }, + { + "required": false, + "description": "Filter by unassigned", + "name": "unassigned", + "in": "query", + "schema": { "type": "boolean" } + }, + { + "required": false, + "description": "Search by main type id", + "name": "mainType", + "in": "query", + "schema": { "type": "string" } + }, + { + "required": false, + "description": "Search by title", "name": "search", - "description": "String to search for", + "in": "query", + "schema": { "type": "string" } + }, + { "required": false, + "description": "Search by slug", + "name": "slug", "in": "query", "schema": { "type": "string" } }, { - "name": "ids", "required": false, + "description": "Search by department slug, title or id", + "name": "department", "in": "query", - "schema": { - "default": [], - "type": "array", - "items": { "type": "string" } - } + "schema": { "type": "string" } }, { - "name": "page", - "description": "Page number to return.", "required": false, + "description": "The page number", + "name": "page", "in": "query", "schema": { "type": "number" } }, { - "name": "pageSize", - "description": "Page size number to return.", "required": false, + "description": "The page size", + "name": "pageSize", "in": "query", "schema": { "type": "number" } } ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetAdvertMainTypes" } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + } + } + } + }, + "/api/v1/advert-types/types/{id}": { + "get": { + "operationId": "getTypeById", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetAdvertType" } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + } + } + } + }, + "/api/v1/advert-types/main-types/{id}": { + "get": { + "operationId": "getMainTypeById", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetAdvertMainType" } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AdvertTypeError" } + } + } + } + } + } + }, + "/api/v1/pdf/case/{id}": { + "get": { + "operationId": "getPdfByCaseId", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], "responses": { "default": { "description": "", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/GetCasesInProgressReponse" - } + "schema": { "$ref": "#/components/schemas/GetPdfRespone" } } } } } } }, - "/api/v1/error": { + "/api/v1/pdf/application/{id}": { "get": { - "operationId": "error", - "parameters": [], + "operationId": "getPdfByApplicationId", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], "responses": { "default": { "description": "", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/ValidationResponse" } + "schema": { "$ref": "#/components/schemas/GetPdfRespone" } + } + } + } + } + } + }, + "/api/v1/pdf/case/{id}/url": { + "get": { + "operationId": "getPdfUrlByCaseId", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetPdfUrlResponse" } + } + } + } + } + } + }, + "/api/v1/pdf/application/{id}/url": { + "get": { + "operationId": "getPdfUrlByApplicationId", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetPdfUrlResponse" } } } } @@ -551,23 +847,18 @@ "properties": { "id": { "type": "string", - "description": "Unique ID for the advert type, GUID format.", - "example": "00000000-0000-0000-0000-000000000000", - "nullable": false + "description": "The id of the main advert type" }, "title": { "type": "string", - "description": "Title of the advert type, always uppercased.", - "example": "AUGLÝSING" + "description": "The title of the main advert type" }, "slug": { "type": "string", - "description": "Slug of the advert type, used in URLs and API requests.", - "example": "auglysing" + "description": "The slug of the main advert type" }, "department": { - "description": "Department the advert type belongs to.", - "nullable": true, + "description": "The department of the main advert type", "allOf": [{ "$ref": "#/components/schemas/Department" }] } }, @@ -977,31 +1268,6 @@ }, "required": ["departments", "paging"] }, - "GetAdvertTypeResponse": { - "type": "object", - "properties": { - "type": { - "description": "Advert type", - "allOf": [{ "$ref": "#/components/schemas/AdvertType" }] - } - }, - "required": ["type"] - }, - "GetAdvertTypesResponse": { - "type": "object", - "properties": { - "types": { - "description": "List of advert types", - "type": "array", - "items": { "$ref": "#/components/schemas/AdvertType" } - }, - "paging": { - "description": "Paging info", - "allOf": [{ "$ref": "#/components/schemas/Paging" }] - } - }, - "required": ["types", "paging"] - }, "MainCategory": { "type": "object", "properties": { @@ -1190,6 +1456,108 @@ } }, "required": ["message", "statusCode"] + }, + "GetAdvertTypes": { + "type": "object", + "properties": { + "types": { + "description": "List of advert types", + "type": "array", + "items": { "$ref": "#/components/schemas/AdvertType" } + }, + "paging": { + "description": "Paging information", + "allOf": [{ "$ref": "#/components/schemas/Paging" }] + } + }, + "required": ["types", "paging"] + }, + "AdvertTypeError": { + "type": "object", + "properties": { + "errorType": { + "type": "string", + "enum": ["DuplicateError", "ValidationError", "NotFoundError"] + }, + "name": { "type": "string" }, + "message": { "type": "string" }, + "severity": { "type": "string", "enum": ["info", "warning", "error"] } + }, + "required": ["errorType", "name", "message", "severity"] + }, + "AdvertMainType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The id of the main advert type" + }, + "title": { + "type": "string", + "description": "The title of the main advert type" + }, + "slug": { + "type": "string", + "description": "The slug of the main advert type" + }, + "department": { + "description": "The department this main type belongs to", + "allOf": [{ "$ref": "#/components/schemas/Department" }] + }, + "types": { + "description": "All types under this main type", + "type": "array", + "items": { "$ref": "#/components/schemas/AdvertType" } + } + }, + "required": ["id", "title", "slug", "department", "types"] + }, + "GetAdvertMainTypes": { + "type": "object", + "properties": { + "mainTypes": { + "description": "List of all main advert types", + "type": "array", + "items": { "$ref": "#/components/schemas/AdvertMainType" } + }, + "paging": { + "description": "Paging information", + "allOf": [{ "$ref": "#/components/schemas/Paging" }] + } + }, + "required": ["mainTypes", "paging"] + }, + "GetAdvertType": { + "type": "object", + "properties": { + "type": { + "description": "The advert type", + "allOf": [{ "$ref": "#/components/schemas/AdvertType" }] + } + }, + "required": ["type"] + }, + "GetAdvertMainType": { + "type": "object", + "properties": { + "mainType": { + "description": "The main advert type", + "allOf": [{ "$ref": "#/components/schemas/AdvertMainType" }] + } + }, + "required": ["mainType"] + }, + "GetPdfRespone": { + "type": "object", + "properties": { + "content": { "type": "string", "description": "Base64 encoded PDF" } + }, + "required": ["content"] + }, + "GetPdfUrlResponse": { + "type": "object", + "properties": { "url": { "type": "string" } }, + "required": ["url"] } } } diff --git a/libs/clients/official-journal-of-iceland/public/src/lib/officialJournalOfIcelandClient.service.ts b/libs/clients/official-journal-of-iceland/public/src/lib/officialJournalOfIcelandClient.service.ts index 1ab4b33951c9..a600c48cb36e 100644 --- a/libs/clients/official-journal-of-iceland/public/src/lib/officialJournalOfIcelandClient.service.ts +++ b/libs/clients/official-journal-of-iceland/public/src/lib/officialJournalOfIcelandClient.service.ts @@ -7,11 +7,13 @@ import { GetDepartmentsRequest, GetInstitutionsRequest, GetMainCategoriesRequest, - GetAdvertTypesRequest, GetDepartmentByIdRequest, - GetAdvertTypeByIdRequest, GetCasesInProgressRequest, + GetTypeByIdRequest, + GetTypesRequest, + GetMainTypesRequest, } from '../../gen/fetch/apis' +import { GetAdvertMainTypes } from '../../gen/fetch' @Injectable() export class OfficialJournalOfIcelandClientService { @@ -33,12 +35,18 @@ export class OfficialJournalOfIcelandClientService { return this.api.getDepartments(params ?? {}) } - public async getAdvertTypeById(params: GetAdvertTypeByIdRequest) { - return this.api.getAdvertTypeById(params) + public async getAdvertTypeById(params: GetTypeByIdRequest) { + return this.api.getTypeById(params) } - public async getAdvertTypes(params: GetAdvertTypesRequest) { - return this.api.getAdvertTypes(params) + public async getAdvertMainTypes( + params: GetMainTypesRequest, + ): Promise { + return this.api.getMainTypes(params) + } + + public async getAdvertTypes(params: GetTypesRequest) { + return this.api.getTypes(params) } public async getMainCategories(params: GetMainCategoriesRequest) { diff --git a/libs/cms/src/lib/generated/contentfulTypes.d.ts b/libs/cms/src/lib/generated/contentfulTypes.d.ts index 24d4c5a81f65..2f49a4508978 100644 --- a/libs/cms/src/lib/generated/contentfulTypes.d.ts +++ b/libs/cms/src/lib/generated/contentfulTypes.d.ts @@ -3249,6 +3249,9 @@ export interface IOrganizationPageFields { /** Sitemap */ sitemap?: ISitemap | undefined + + /** Can be found in search results */ + canBeFoundInSearchResults?: boolean | undefined } export interface IOrganizationPage extends Entry { diff --git a/libs/cms/src/lib/models/organizationPage.model.ts b/libs/cms/src/lib/models/organizationPage.model.ts index 4ca0e4b3eb98..233b62efafff 100644 --- a/libs/cms/src/lib/models/organizationPage.model.ts +++ b/libs/cms/src/lib/models/organizationPage.model.ts @@ -86,6 +86,9 @@ export class OrganizationPage { @CacheField(() => OrganizationPageTopLevelNavigation, { nullable: true }) topLevelNavigation?: OrganizationPageTopLevelNavigation | null + + @Field(() => Boolean, { nullable: true }) + canBeFoundInSearchResults?: boolean } export const mapOrganizationPage = ({ @@ -144,5 +147,6 @@ export const mapOrganizationPage = ({ ? mapImage(fields.defaultHeaderImage) : undefined, topLevelNavigation, + canBeFoundInSearchResults: fields.canBeFoundInSearchResults ?? true, } } diff --git a/libs/cms/src/lib/search/importers/organizationPage.service.ts b/libs/cms/src/lib/search/importers/organizationPage.service.ts index 9a60a31bf272..984d64527323 100644 --- a/libs/cms/src/lib/search/importers/organizationPage.service.ts +++ b/libs/cms/src/lib/search/importers/organizationPage.service.ts @@ -15,7 +15,8 @@ export class OrganizationPageSyncService return entries.filter( (entry: Entry): entry is IOrganizationPage => entry.sys.contentType.sys.id === 'organizationPage' && - !!entry.fields.title, + !!entry.fields.title && + (entry.fields.canBeFoundInSearchResults ?? true), ) } diff --git a/libs/cms/src/lib/search/importers/organizationSubpage.service.ts b/libs/cms/src/lib/search/importers/organizationSubpage.service.ts index 7edf40d5a224..5113a03ff260 100644 --- a/libs/cms/src/lib/search/importers/organizationSubpage.service.ts +++ b/libs/cms/src/lib/search/importers/organizationSubpage.service.ts @@ -24,7 +24,10 @@ export class OrganizationSubpageSyncService !!entry.fields.slug && !!entry.fields.organizationPage?.fields?.slug && // Standalone organization pages have their own search, we don't want subpages there to be found in the global search - entry.fields.organizationPage.fields.theme !== 'standalone', + entry.fields.organizationPage.fields.theme !== 'standalone' && + // Subpage should not be searchable if the organization frontpage isn't searchable + (entry.fields.organizationPage.fields.canBeFoundInSearchResults ?? + true), ) } diff --git a/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx b/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx index 4c6adaac0d1e..28952dcef45c 100644 --- a/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx +++ b/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx @@ -38,6 +38,7 @@ export const Breadcrumbs: FC> = ({ {d.text} diff --git a/libs/island-ui/core/src/lib/InfoCardGrid/InfoCard.css.ts b/libs/island-ui/core/src/lib/InfoCardGrid/InfoCard.css.ts index 4c27d84947de..d010a5ddeecc 100644 --- a/libs/island-ui/core/src/lib/InfoCardGrid/InfoCard.css.ts +++ b/libs/island-ui/core/src/lib/InfoCardGrid/InfoCard.css.ts @@ -1,4 +1,4 @@ -import { style } from '@vanilla-extract/css' +import { globalStyle, style } from '@vanilla-extract/css' export const infoCardSmall = style({ maxWidth: 310, @@ -18,3 +18,9 @@ export const infoCardWide = style({ export const wideTitleBox = style({ flexGrow: 2, }) + +export const iconBox = style({}) + +globalStyle(`${iconBox} > svg`, { + minWidth: 24, +}) diff --git a/libs/judicial-system/message/src/lib/message.ts b/libs/judicial-system/message/src/lib/message.ts index bb32cdc84e05..2472dfa4c37c 100644 --- a/libs/judicial-system/message/src/lib/message.ts +++ b/libs/judicial-system/message/src/lib/message.ts @@ -11,6 +11,7 @@ export enum MessageType { DELIVERY_TO_COURT_CASE_FILE = 'DELIVERY_TO_COURT_CASE_FILE', DELIVERY_TO_COURT_CASE_FILES_RECORD = 'DELIVERY_TO_COURT_CASE_FILES_RECORD', DELIVERY_TO_COURT_REQUEST = 'DELIVERY_TO_COURT_REQUEST', + DELIVERY_TO_COURT_SUBPOENA = 'DELIVERY_TO_COURT_SUBPOENA', DELIVERY_TO_COURT_COURT_RECORD = 'DELIVERY_TO_COURT_COURT_RECORD', DELIVERY_TO_COURT_SIGNED_RULING = 'DELIVERY_TO_COURT_SIGNED_RULING', DELIVERY_TO_COURT_CASE_CONCLUSION = 'DELIVERY_TO_COURT_CASE_CONCLUSION', @@ -49,6 +50,7 @@ export const messageEndpoint: { [key in MessageType]: string } = { DELIVERY_TO_COURT_CASE_FILE: 'deliverCaseFileToCourt', DELIVERY_TO_COURT_CASE_FILES_RECORD: 'deliverCaseFilesRecordToCourt', DELIVERY_TO_COURT_REQUEST: 'deliverRequestToCourt', + DELIVERY_TO_COURT_SUBPOENA: 'deliverSubpoenaToCourt', DELIVERY_TO_COURT_COURT_RECORD: 'deliverCourtRecordToCourt', DELIVERY_TO_COURT_SIGNED_RULING: 'deliverSignedRulingToCourt', DELIVERY_TO_COURT_CASE_CONCLUSION: 'deliverCaseConclusionToCourt', diff --git a/libs/judicial-system/types/src/lib/eventLog.ts b/libs/judicial-system/types/src/lib/eventLog.ts index fca7b91f81ea..5aa84c863960 100644 --- a/libs/judicial-system/types/src/lib/eventLog.ts +++ b/libs/judicial-system/types/src/lib/eventLog.ts @@ -15,6 +15,7 @@ export const eventTypes = Object.values(EventType) export enum DefendantEventType { SENT_TO_PRISON_ADMIN = 'SENT_TO_PRISON_ADMIN', + OPENED_BY_PRISON_ADMIN = 'OPENED_BY_PRISON_ADMIN', } export const defendantEventTypes = Object.values(DefendantEventType) diff --git a/libs/portals/my-pages/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSubData.tsx b/libs/portals/my-pages/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSubData.tsx index 913851852c79..13d1dac55d2d 100644 --- a/libs/portals/my-pages/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSubData.tsx +++ b/libs/portals/my-pages/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSubData.tsx @@ -3,6 +3,7 @@ import { NestedFullTable, SimpleBarChart, formatDate, + numberFormat, } from '@island.is/portals/my-pages/core' import { Box, Text, Button } from '@island.is/island-ui/core' import { AssetsPaths } from '../../lib/paths' @@ -137,6 +138,7 @@ export const VehicleBulkMileageSubData = ({ labels: { mileage: formatMessage(vehicleMessage.odometer), }, + valueFormat: (arg: number) => `${numberFormat(arg)} km`, }} /> ) : undefined} diff --git a/libs/portals/my-pages/core/src/components/Charts/SimpleBarChart/SimpleBarChart.tsx b/libs/portals/my-pages/core/src/components/Charts/SimpleBarChart/SimpleBarChart.tsx index 419ad09aaa11..ba7bd5427c41 100644 --- a/libs/portals/my-pages/core/src/components/Charts/SimpleBarChart/SimpleBarChart.tsx +++ b/libs/portals/my-pages/core/src/components/Charts/SimpleBarChart/SimpleBarChart.tsx @@ -21,6 +21,7 @@ import { theme } from '@island.is/island-ui/theme' interface Axis { label?: string datakey: string + valueFormat?: (arg: number) => string } interface BarType { @@ -29,6 +30,7 @@ interface BarType { interface TooltipType { labels: Record + valueFormat?: (arg: number) => string } interface GraphDataProps { @@ -96,7 +98,12 @@ export const SimpleBarChart = ({ /> } /> } + content={ + + } /> string } interface CustomTooltipProps extends TooltipProps { valueLabels?: Record + valueFormat?: (arg: number) => string } export const CustomTooltip = ({ @@ -20,25 +23,34 @@ export const CustomTooltip = ({ active, label, valueLabels, + valueFormat, }: CustomTooltipProps) => { if (active && payload && payload.length) { return ( {label} - {payload.map((item, index) => ( - -
- - {valueLabels && item.name ? valueLabels[item.name] : item.name} : - {item.value} - - - ))} + {payload + .map((item, index) => { + if (!item.value) return null + + return ( + +
+ + {valueLabels && item.dataKey + ? valueLabels[item.dataKey] + : item.name} + : {valueFormat ? valueFormat(item.value) : item.value} + + + ) + }) + .filter(isDefined)} ) } diff --git a/libs/portals/my-pages/health/src/lib/messages.ts b/libs/portals/my-pages/health/src/lib/messages.ts index d22f5798bed3..67c32b9383c0 100644 --- a/libs/portals/my-pages/health/src/lib/messages.ts +++ b/libs/portals/my-pages/health/src/lib/messages.ts @@ -1024,10 +1024,22 @@ export const messages = defineMessages({ defaultMessage: 'Textareitur má ekki vera tómur sé þessi valkostur valinn. ', }, + organMinor: { + id: 'sp.health:organ-minor', + defaultMessage: 'Til að geta gerst líffæragjafi þarftu að vera 18 ára.', + }, + organTemporaryNationalId: { + id: 'sp.health:organ-temporary-national-id', + defaultMessage: 'Líffæragjöf er ekki heimiluð á kerfiskennitölur.', + }, other: { id: 'sp.health:other-lower-case', defaultMessage: 'annað', }, + otherPascalCase: { + id: 'sp.health:other', + defaultMessage: 'Annað', + }, registrationComplete: { id: 'sp.health:registration-complete', defaultMessage: 'Skráning tókst', diff --git a/libs/portals/my-pages/health/src/module.tsx b/libs/portals/my-pages/health/src/module.tsx index 5d9cc4fdfccd..2f6f92805030 100644 --- a/libs/portals/my-pages/health/src/module.tsx +++ b/libs/portals/my-pages/health/src/module.tsx @@ -59,7 +59,7 @@ const OrganDonation = lazy(() => ) const OrganDonationRegistration = lazy(() => - import('./screens/OrganDonationRegistration/RegistrationForm'), + import('./screens/OrganDonation/components/RegistrationForm'), ) const Vaccinations = lazy(() => diff --git a/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.css.ts b/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.css.ts new file mode 100644 index 000000000000..4dfc4127851b --- /dev/null +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.css.ts @@ -0,0 +1,32 @@ +import { style, keyframes } from '@vanilla-extract/css' +import { theme } from '@island.is/island-ui/theme' + +export const buttonContainer = style({ + gap: theme.spacing[2], +}) + +const fadeIn = keyframes({ + from: { + opacity: 0, + }, + to: { + opacity: 1, + }, +}) + +const fadeOut = keyframes({ + from: { + opacity: 1, + }, + to: { + opacity: 0, + }, +}) + +export const commentVisible = style({ + animation: `${fadeIn} 0.5s forwards`, +}) + +export const commentHidden = style({ + animation: `${fadeOut} 0.5s forwards`, +}) diff --git a/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.graphql b/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.graphql index d120a592b167..6790da16ee2b 100644 --- a/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.graphql +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.graphql @@ -10,21 +10,8 @@ query getDonorStatus($locale: String) { } comment } - } - } -} - -query getOrgansList($locale: String) { - healthDirectorateOrganDonation(locale: $locale) { - donor { - isDonor - limitations { - hasLimitations - limitedOrgansList { - id - name - } - } + isMinor + isTemporaryResident } organList { id diff --git a/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.tsx b/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.tsx index 7efcd493e3b5..7ef00c370fb5 100644 --- a/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.tsx +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/OrganDonation.tsx @@ -1,15 +1,17 @@ +import { Box, Button, Text } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' import { ActionCard, CardLoader, - IntroHeader, + IntroWrapper, LinkResolver, } from '@island.is/portals/my-pages/core' +import { Problem } from '@island.is/react-spa/shared' import { messages as m } from '../../lib/messages' -import { Button, Box, Text } from '@island.is/island-ui/core' import { HealthPaths } from '../../lib/paths' -import { Problem } from '@island.is/react-spa/shared' import { useGetDonorStatusQuery } from './OrganDonation.generated' +import { NoAccess } from './components/NoAccess' +import { getOrganText } from './helpers/textMapper' const OrganDonation = () => { useNamespaces('sp.health') @@ -22,30 +24,39 @@ const OrganDonation = () => { }, }) const donorStatus = data?.healthDirectorateOrganDonation.donor - const cardText: string = donorStatus?.isDonor - ? donorStatus?.limitations?.hasLimitations - ? [ - formatMessage(m.iAmOrganDonorWithExceptionsText), - donorStatus?.limitations.limitedOrgansList - ?.map((organ) => organ.name) - .join(', '), - ].join(' ') + '.' - : formatMessage(m.iAmOrganDonorText) - : formatMessage(m.iAmNotOrganDonorText) + const isMinor = donorStatus?.isMinor + const isTemporaryResident = donorStatus?.isTemporaryResident + + const comment = donorStatus?.limitations?.comment + + const allLimitations: Array = + donorStatus?.limitations?.limitedOrgansList?.map((item) => item.name) ?? [] - const heading = donorStatus?.isDonor - ? donorStatus.limitations?.hasLimitations - ? formatMessage(m.iAmOrganDonorWithExceptions) - : formatMessage(m.iAmOrganDonor) - : formatMessage(m.iAmNotOrganDonor) + if (comment !== undefined && comment !== null && comment.length > 0) { + allLimitations.push(comment) + } + + const texts = getOrganText( + donorStatus?.isDonor ?? true, + donorStatus?.limitations?.hasLimitations ?? false, + { + iAmOrganDonorWithExceptionsText: formatMessage( + m.iAmOrganDonorWithExceptionsText, + ), + iAmNotOrganDonorText: formatMessage(m.iAmOrganDonorText), + iAmOrganDonorText: formatMessage(m.iAmNotOrganDonorText), + iAmOrganDonorWithExceptions: formatMessage(m.iAmOrganDonorWithExceptions), + iAmOrganDonor: formatMessage(m.iAmOrganDonor), + iAmNotOrganDonor: formatMessage(m.iAmNotOrganDonor), + }, + allLimitations, + ) return ( - - - + { - - + , + ]} + > {loading && ( )} - {!error && !loading && donorStatus !== null && ( - - - {formatMessage(m.takeOnOrganDonation)} - - - + {!error && + !loading && + !isMinor && + !isTemporaryResident && + donorStatus !== null && ( + + + {formatMessage(m.takeOnOrganDonation)} + + + + )} + {!error && !loading && (isMinor || isTemporaryResident) && ( + )} {error && !loading && } {!error && !loading && data === null && ( )} - + ) } diff --git a/libs/portals/my-pages/health/src/screens/OrganDonation/components/Limitations.tsx b/libs/portals/my-pages/health/src/screens/OrganDonation/components/Limitations.tsx new file mode 100644 index 000000000000..ab0e895e30a3 --- /dev/null +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/components/Limitations.tsx @@ -0,0 +1,113 @@ +import { HealthDirectorateOrganDonationOrgan } from '@island.is/api/schema' +import { + Box, + Checkbox, + Divider, + GridColumn, + GridContainer, + GridRow, + Input, + Stack, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { useState } from 'react' +import { messages } from '../../../lib/messages' +import * as styles from '../OrganDonation.css' + +interface LimitationsProps { + data: HealthDirectorateOrganDonationOrgan[] + selected?: string[] | null + exceptionComment?: string +} + +const Limitations = ({ + data, + selected, + exceptionComment, +}: LimitationsProps) => { + useNamespaces('sp.health') + const { formatMessage } = useLocale() + const [checked, setChecked] = useState>(selected ?? []) + const [comment, setComment] = useState(exceptionComment ?? '') + + const handleCheckboxChange = (id: string, isChecked: boolean) => { + setChecked((prevState) => + isChecked ? [...prevState, id] : prevState.filter((item) => item !== id), + ) + } + + return ( + + + + + {data?.map((y, yi) => ( + + + handleCheckboxChange(y.id ?? '', e.target.checked) + } + checked={checked.includes(y.id ?? '')} + /> + + ))} + + { + setComment(e.target.value) + handleCheckboxChange('other', e.target.checked) + }} + checked={checked.includes('other')} + /> + + + + {checked.includes('other') && ( + + + + + setComment(e.target.value)} + value={comment} + /> + + + + + )} + + ) +} + +export default Limitations diff --git a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/Loader.tsx b/libs/portals/my-pages/health/src/screens/OrganDonation/components/Loader.tsx similarity index 100% rename from libs/portals/my-pages/health/src/screens/OrganDonationRegistration/Loader.tsx rename to libs/portals/my-pages/health/src/screens/OrganDonation/components/Loader.tsx index b907c382c105..59bf49245f20 100644 --- a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/Loader.tsx +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/components/Loader.tsx @@ -1,6 +1,6 @@ -import React from 'react' import { Box, SkeletonLoader, Stack } from '@island.is/island-ui/core' import { useIsMobile } from '@island.is/portals/my-pages/core' +import React from 'react' interface Props { amount?: number diff --git a/libs/portals/my-pages/health/src/screens/OrganDonation/components/NoAccess.tsx b/libs/portals/my-pages/health/src/screens/OrganDonation/components/NoAccess.tsx new file mode 100644 index 000000000000..369589995cd1 --- /dev/null +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/components/NoAccess.tsx @@ -0,0 +1,13 @@ +import { AlertMessage, GridColumn, GridRow } from '@island.is/island-ui/core' + +export const NoAccess = ({ text }: { text: string }) => { + return ( + + + + + + ) +} + +export default NoAccess diff --git a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/RegistrationForm.tsx b/libs/portals/my-pages/health/src/screens/OrganDonation/components/RegistrationForm.tsx similarity index 77% rename from libs/portals/my-pages/health/src/screens/OrganDonationRegistration/RegistrationForm.tsx rename to libs/portals/my-pages/health/src/screens/OrganDonation/components/RegistrationForm.tsx index 6ef3db623dcf..4b4db5a87fcf 100644 --- a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/RegistrationForm.tsx +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/components/RegistrationForm.tsx @@ -1,58 +1,68 @@ import { Box, + Button, RadioButton, Stack, Text, - Button, toast, - LoadingDots, } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' import { - IntroHeader, + IntroWrapper, LinkResolver, m as coreMessages, } from '@island.is/portals/my-pages/core' -import { messages } from '../..' -import { useEffect, useState } from 'react' -import React from 'react' -import { HealthPaths } from '../../lib/paths' -import * as styles from './OrganDonationRegistration.css' -import Limitations from './Limitations' +import React, { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { messages } from '../../..' +import { HealthPaths } from '../../../lib/paths' +import * as styles from '../OrganDonation.css' import { - useGetOrgansListQuery, + useGetDonorStatusQuery, useUpdateOrganDonationInfoMutation, -} from '../OrganDonation/OrganDonation.generated' +} from '../OrganDonation.generated' +import Limitations from './Limitations' import { Loader } from './Loader' +import { NoAccess } from './NoAccess' const OPT_IN = 'opt-in' const OPT_IN_EXCEPTIONS = 'opt-in-exceptions' const OPT_OUT = 'opt-out' -export const Form2 = () => { +export const OrganRegistrationForm = () => { useNamespaces('sp.health') const { formatMessage, lang } = useLocale() const navigate = useNavigate() - const { data, loading } = useGetOrgansListQuery({ + const { data, loading } = useGetDonorStatusQuery({ variables: { locale: lang }, fetchPolicy: 'no-cache', }) const isDonor = data?.healthDirectorateOrganDonation.donor?.isDonor + const isMinor = data?.healthDirectorateOrganDonation.donor?.isMinor + const isTemporaryResident = + data?.healthDirectorateOrganDonation.donor?.isTemporaryResident const hasLimitations = data?.healthDirectorateOrganDonation.donor?.limitations?.hasLimitations const allLimitations = data?.healthDirectorateOrganDonation.organList + const exceptionComment = + data?.healthDirectorateOrganDonation.donor?.limitations?.comment + const selectedLimitations = data?.healthDirectorateOrganDonation.donor?.limitations?.limitedOrgansList?.map( (item) => item.id, ) + + const updatedLimitations = selectedLimitations + ? [...selectedLimitations, ...(exceptionComment?.length ? ['other'] : [])] + : [] const donorStatus = isDonor ? hasLimitations ? OPT_IN_EXCEPTIONS : OPT_IN : OPT_OUT + const [radioValue, setRadioValue] = useState(donorStatus) const [updateDonorStatus, { loading: submitLoading }] = @@ -76,8 +86,8 @@ export const Form2 = () => { e.preventDefault() const formData = new FormData(e.currentTarget) const data = Object.fromEntries(formData.entries()) - const idKey = 'selected-limitations-' + const otherLimitations = data['otherLimitatons'].toString() const limitations = Object.keys(data) .filter((key) => key.includes(idKey)) .map((key) => key.replace(idKey, '').toLowerCase()) @@ -87,6 +97,7 @@ export const Form2 = () => { input: { isDonor: radioValue === OPT_IN || radioValue === OPT_IN_EXCEPTIONS, organLimitations: radioValue === OPT_IN_EXCEPTIONS ? limitations : [], + comment: otherLimitations, }, locale: lang, }, @@ -94,16 +105,24 @@ export const Form2 = () => { } return ( - - + {formatMessage(messages.changeTake)} {loading && } - {!loading && ( + {!loading && (isMinor || isTemporaryResident) && ( + + )} + {!loading && !isMinor && !isTemporaryResident && (
{ radioValue === OPT_IN_EXCEPTIONS && ( )} @@ -187,8 +207,8 @@ export const Form2 = () => { )} -
+ ) } -export default Form2 +export default OrganRegistrationForm diff --git a/libs/portals/my-pages/health/src/screens/OrganDonation/helpers/textMapper.ts b/libs/portals/my-pages/health/src/screens/OrganDonation/helpers/textMapper.ts new file mode 100644 index 000000000000..7793edbf10bf --- /dev/null +++ b/libs/portals/my-pages/health/src/screens/OrganDonation/helpers/textMapper.ts @@ -0,0 +1,27 @@ +export const getOrganText = ( + isDonor: boolean, + hasLimitations: boolean, + texts: { + iAmOrganDonorWithExceptionsText: string + iAmOrganDonorText: string + iAmNotOrganDonorText: string + iAmOrganDonorWithExceptions: string + iAmOrganDonor: string + iAmNotOrganDonor: string + }, + limitations: string[], +) => { + const limitationText = hasLimitations + ? texts.iAmOrganDonorWithExceptionsText + ' ' + limitations.join(', ') + : texts.iAmOrganDonorText + + const cardText: string = isDonor ? limitationText : texts.iAmNotOrganDonorText + + const heading = isDonor + ? hasLimitations + ? texts.iAmOrganDonorWithExceptions + : texts.iAmOrganDonor + : texts.iAmNotOrganDonor + + return { cardText, heading } +} diff --git a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/Limitations.tsx b/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/Limitations.tsx deleted file mode 100644 index e625cdf7a3a4..000000000000 --- a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/Limitations.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Box, Checkbox, Divider, Stack } from '@island.is/island-ui/core' -import React, { useState } from 'react' -import { useNamespaces } from '@island.is/localization' -import { HealthDirectorateOrganDonationOrgan } from '@island.is/api/schema' - -interface LimitationsProps { - data: HealthDirectorateOrganDonationOrgan[] - selected?: string[] | null -} - -const Limitations = ({ data, selected }: LimitationsProps) => { - useNamespaces('sp.health') - const [checked, setChecked] = useState>(selected ?? []) - const handleCheckboxChange = (id: string, isChecked: boolean) => { - setChecked((prevState) => - isChecked ? [...prevState, id] : prevState.filter((item) => item !== id), - ) - } - - //const input = data.find((x) => x.type === 'input') - - return ( - - - - - {data?.map( - (y, yi) => ( - // y.type === 'checkbox' && ( - - - - handleCheckboxChange(y.id ?? '', e.target.checked) - } - checked={checked.includes(y.id ?? '')} - /> - - ), - // ), - )} - - - {/* This is commented out because of feature that was removed. May be included later on */} - {/* {input && checked.includes(input.name.toLowerCase()) && ( - - - - - - - - - - )} */} - - ) -} - -export default Limitations diff --git a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/OrganDonationRegistration.css.ts b/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/OrganDonationRegistration.css.ts deleted file mode 100644 index e985fe3863ac..000000000000 --- a/libs/portals/my-pages/health/src/screens/OrganDonationRegistration/OrganDonationRegistration.css.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { style } from '@vanilla-extract/css' -import { theme } from '@island.is/island-ui/theme' - -export const buttonContainer = style({ - gap: theme.spacing[2], -})