diff --git a/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts index 1c663df0edfd..f5798cf30849 100644 --- a/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts @@ -4,14 +4,14 @@ import { formatDate, lowercase } from '@island.is/judicial-system/formatters' import { calculatePt, + Confirmation, drawTextWithEllipsisPDFKit, - IndictmentConfirmation, smallFontSize, } from './pdfHelpers' import { PDFKitCoatOfArms } from './PDFKitCoatOfArms' export const createConfirmedIndictment = async ( - confirmation: IndictmentConfirmation, + confirmation: Confirmation, indictmentPDF: Buffer, ): Promise => { const pdfDoc = await PDFDocument.load(indictmentPDF) diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index 084da81ca6b5..120d7279c56e 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -29,7 +29,7 @@ export { formatPostponedCourtDateEmailNotification, stripHtmlTags, } from './formatters' -export { IndictmentConfirmation } from './pdfHelpers' +export { Confirmation } from './pdfHelpers' export { getRequestPdfAsBuffer, getRequestPdfAsString } from './requestPdf' export { getRulingPdfAsBuffer, getRulingPdfAsString } from './rulingPdf' export { createCaseFilesRecord } from './caseFilesRecordPdf' diff --git a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts index c19da2711a7f..5ab70b81c81a 100644 --- a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts @@ -16,7 +16,7 @@ import { addNormalPlusJustifiedText, addNormalPlusText, addNormalText, - IndictmentConfirmation, + Confirmation, setTitle, } from './pdfHelpers' @@ -52,7 +52,7 @@ const roman = (num: number) => { export const createIndictment = async ( theCase: Case, formatMessage: FormatMessage, - confirmation?: IndictmentConfirmation, + confirmation?: Confirmation, ): Promise => { const doc = new PDFDocument({ size: 'A4', diff --git a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts index d348032ebc35..4f44aa249e93 100644 --- a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts +++ b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts @@ -5,7 +5,7 @@ import { formatDate, lowercase } from '@island.is/judicial-system/formatters' import { coatOfArms } from './coatOfArms' import { policeStar } from './policeStar' -export interface IndictmentConfirmation { +export interface Confirmation { actor: string title?: string institution: string @@ -22,6 +22,10 @@ export const largeFontSize = 18 export const hugeFontSize = 26 export const giganticFontSize = 33 +const lightGray = '#FAFAFA' +const darkGray = '#CBCBCB' +const gold = '#ADA373' + const setFont = (doc: PDFKit.PDFDocument, font?: string) => { if (font) { doc.font(font) @@ -106,13 +110,105 @@ export const addPoliceStar = (doc: PDFKit.PDFDocument) => { doc.scale(25).translate(-270, -70) } +export const addConfirmation = ( + doc: PDFKit.PDFDocument, + confirmation: Confirmation, +) => { + const pageMargin = calculatePt(18) + const shaddowHeight = calculatePt(70) + const coatOfArmsWidth = calculatePt(105) + const coatOfArmsX = pageMargin + calculatePt(8) + const titleHeight = calculatePt(24) + const titleX = coatOfArmsX + coatOfArmsWidth + calculatePt(8) + const institutionWidth = calculatePt(160) + const confirmedByWidth = institutionWidth + calculatePt(48) + const shaddowWidth = institutionWidth + confirmedByWidth + coatOfArmsWidth + const titleWidth = institutionWidth + confirmedByWidth + + // Draw the shadow + doc + .rect(pageMargin, pageMargin + calculatePt(8), shaddowWidth, shaddowHeight) + .fill(lightGray) + .stroke() + + // Draw the coat of arms + doc + .rect(coatOfArmsX, pageMargin, coatOfArmsWidth, shaddowHeight) + .fillAndStroke('white', darkGray) + + addCoatOfArms(doc, calculatePt(49), calculatePt(24)) + + // Draw the title + doc + .rect(coatOfArmsX + coatOfArmsWidth, pageMargin, titleWidth, titleHeight) + .fillAndStroke(lightGray, darkGray) + doc.fill('black') + doc.font('Times-Bold') + doc + .fontSize(calculatePt(smallFontSize)) + .text('Réttarvörslugátt', titleX, pageMargin + calculatePt(9)) + doc.font('Times-Roman') + // The X value here is approx. 8px after the title + doc.text('Rafræn staðfesting', calculatePt(210), pageMargin + calculatePt(9)) + doc.text( + formatDate(confirmation.date) || '', + shaddowWidth - calculatePt(24), + pageMargin + calculatePt(9), + ) + + // Draw the institution + doc + .rect( + coatOfArmsX + coatOfArmsWidth, + pageMargin + titleHeight, + institutionWidth, + shaddowHeight - titleHeight, + ) + .fillAndStroke('white', darkGray) + doc.fill('black') + doc.font('Times-Bold') + doc.text('Dómstóll', titleX, pageMargin + titleHeight + calculatePt(10)) + doc.font('Times-Roman') + drawTextWithEllipsis( + doc, + confirmation.institution, + titleX, + pageMargin + titleHeight + calculatePt(22), + institutionWidth - calculatePt(16), + ) + + // Draw the actor + doc + .rect( + coatOfArmsX + coatOfArmsWidth + institutionWidth, + pageMargin + titleHeight, + confirmedByWidth, + shaddowHeight - titleHeight, + ) + .fillAndStroke('white', darkGray) + doc.fill('black') + doc.font('Times-Bold') + doc.text( + 'Samþykktaraðili', + titleX + institutionWidth, + pageMargin + titleHeight + calculatePt(10), + ) + doc.font('Times-Roman') + doc.text( + `${confirmation.actor}${ + confirmation.title ? `, ${lowercase(confirmation.title)}` : '' + }`, + titleX + institutionWidth, + pageMargin + titleHeight + calculatePt(22), + ) + + doc.fillColor('black') +} + export const addIndictmentConfirmation = ( doc: PDFKit.PDFDocument, - confirmation: IndictmentConfirmation, + confirmation: Confirmation, ) => { - const lightGray = '#FAFAFA' - const darkGray = '#CBCBCB' - const gold = '#ADA373' const pageMargin = calculatePt(18) const shaddowHeight = calculatePt(90) const coatOfArmsWidth = calculatePt(105) diff --git a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts index 313088079040..4af8001053e8 100644 --- a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts @@ -13,6 +13,7 @@ import { subpoena as strings } from '../messages' import { Case } from '../modules/case' import { Defendant } from '../modules/defendant' import { + addConfirmation, addEmptyLines, addFooter, addHugeHeading, @@ -49,6 +50,11 @@ export const createSubpoena = ( doc.on('data', (chunk) => sinc.push(chunk)) setTitle(doc, formatMessage(strings.title)) + + if (dateLog) { + addEmptyLines(doc, 5) + } + addNormalText(doc, `${theCase.court?.name}`, 'Times-Bold', true) addNormalRightAlignedText( @@ -148,6 +154,15 @@ export const createSubpoena = ( addFooter(doc) + if (dateLog) { + addConfirmation(doc, { + actor: theCase.judge?.name || '', + title: theCase.judge?.title, + institution: theCase.judge?.institution?.name || '', + date: dateLog.created, + }) + } + doc.end() return new Promise((resolve) => diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts index cca384ac525a..7b0da96f91d1 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts @@ -68,7 +68,7 @@ import { prosecutorRule, publicProsecutorStaffRule, } from '../../guards' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { UserService } from '../user' import { CreateCaseDto } from './dto/createCase.dto' import { TransitionCaseDto } from './dto/transitionCase.dto' @@ -99,8 +99,8 @@ import { prosecutorUpdateRule, publicProsecutorStaffUpdateRule, } from './guards/rolesRules' -import { CaseInterceptor } from './interceptors/case.interceptor' import { CaseListInterceptor } from './interceptors/caseList.interceptor' +import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' import { Case } from './models/case.model' import { SignatureConfirmationResponse } from './models/signatureConfirmation.response' import { transitionCase } from './state/case.state' @@ -465,7 +465,7 @@ export class CaseController { ) @Get('case/:caseId') @ApiOkResponse({ type: Case, description: 'Gets an existing case' }) - @UseInterceptors(CaseInterceptor) + @UseInterceptors(CompletedAppealAccessedInterceptor) getById(@Param('caseId') caseId: string, @CurrentCase() theCase: Case): Case { this.logger.debug(`Getting case ${caseId} by id`) @@ -545,6 +545,7 @@ export class CaseController { @RolesRules( prosecutorRule, prosecutorRepresentativeRule, + publicProsecutorStaffRule, districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, @@ -700,6 +701,7 @@ export class CaseController { @RolesRules( prosecutorRule, prosecutorRepresentativeRule, + publicProsecutorStaffRule, districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, 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 65a7daca3a84..eabf3a6cee0b 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 @@ -56,7 +56,7 @@ import { import { AwsS3Service } from '../aws-s3' import { CourtService } from '../court' import { Defendant, DefendantService } from '../defendant' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { EventLog, EventLogService } from '../event-log' import { CaseFile, FileService } from '../file' import { IndictmentCount } from '../indictment-count' diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts b/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts index 460480edf5f8..f92e78361b74 100644 --- a/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts @@ -14,7 +14,7 @@ export class LimitedAccessCaseExistsGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest() - const caseId = request.params.caseId + const caseId: string = request.params.caseId if (!caseId) { throw new BadRequestException('Missing case id') diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts new file mode 100644 index 000000000000..d7d74fb30a5c --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts @@ -0,0 +1,53 @@ +import { Observable } from 'rxjs' +import { map } from 'rxjs/operators' + +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common' + +import { + CaseAppealState, + CaseFileCategory, + isDefenceUser, + isPrisonStaffUser, + isPrisonSystemUser, + User, +} from '@island.is/judicial-system/types' + +import { Case } from '../models/case.model' + +@Injectable() +export class CaseFileInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest() + const user: User = request.user + + return next.handle().pipe( + map((data: Case) => { + if (isDefenceUser(user)) { + return data + } + + if ( + isPrisonStaffUser(user) || + data.appealState !== CaseAppealState.COMPLETED + ) { + data.caseFiles?.splice(0, data.caseFiles.length) + } else if (isPrisonSystemUser(user)) { + data.caseFiles?.splice( + 0, + data.caseFiles.length, + ...data.caseFiles.filter( + (cf) => cf.category === CaseFileCategory.APPEAL_RULING, + ), + ) + } + + return data + }), + ) + } +} 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/completedAppealAccessed.interceptor.ts similarity index 94% rename from apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts rename to apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts index 6beab2bc3915..5ff8d84bff3f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts @@ -20,7 +20,7 @@ import { EventLogService } from '../../event-log' import { Case } from '../models/case.model' @Injectable() -export class CaseInterceptor implements NestInterceptor { +export class CompletedAppealAccessedInterceptor implements NestInterceptor { constructor(private readonly eventLogService: EventLogService) {} intercept(context: ExecutionContext, next: CallHandler): Observable { 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 9149585ce497..0d73789d2686 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 @@ -23,7 +23,7 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { DeliverDto } from './dto/deliver.dto' import { DeliverCancellationNoticeDto } from './dto/deliverCancellationNotice.dto' import { InternalCasesDto } from './dto/internalCases.dto' diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts index ebee2422a741..235d74f860a0 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts @@ -53,7 +53,7 @@ import { AwsS3Service } from '../aws-s3' import { CourtDocumentFolder, CourtService } from '../court' import { courtSubtypes } from '../court/court.service' import { Defendant, DefendantService } from '../defendant' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { CaseFile, FileService } from '../file' import { IndictmentCount, IndictmentCountService } from '../indictment-count' import { Institution } from '../institution' 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 7e17c55bab85..42c90d58ec5e 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 @@ -40,7 +40,7 @@ import { import { nowFactory } from '../../factories' import { defenderRule, prisonSystemStaffRule } from '../../guards' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { User } from '../user' import { TransitionCaseDto } from './dto/transitionCase.dto' import { UpdateCaseDto } from './dto/updateCase.dto' @@ -53,7 +53,8 @@ import { CaseWriteGuard } from './guards/caseWrite.guard' import { LimitedAccessCaseExistsGuard } from './guards/limitedAccessCaseExists.guard' import { RequestSharedWithDefenderGuard } from './guards/requestSharedWithDefender.guard' import { defenderTransitionRule, defenderUpdateRule } from './guards/rolesRules' -import { CaseInterceptor } from './interceptors/case.interceptor' +import { CaseFileInterceptor } from './interceptors/caseFile.interceptor' +import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' import { Case } from './models/case.model' import { transitionCase } from './state/case.state' import { @@ -85,7 +86,7 @@ export class LimitedAccessCaseController { type: Case, description: 'Gets a limited set of properties of an existing case', }) - @UseInterceptors(CaseInterceptor) + @UseInterceptors(CompletedAppealAccessedInterceptor, CaseFileInterceptor) async getById( @Param('caseId') caseId: string, @CurrentCase() theCase: Case, diff --git a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts index 8289b8d636ec..6b956856ebb1 100644 --- a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts @@ -22,6 +22,7 @@ import { } from '@island.is/judicial-system/types' import { + Confirmation, createCaseFilesRecord, createIndictment, createSubpoena, @@ -29,7 +30,6 @@ import { getCustodyNoticePdfAsBuffer, getRequestPdfAsBuffer, getRulingPdfAsBuffer, - IndictmentConfirmation, } from '../../formatters' import { AwsS3Service } from '../aws-s3' import { Defendant } from '../defendant' @@ -206,7 +206,7 @@ export class PdfService { ) } - let confirmation: IndictmentConfirmation | undefined = undefined + let confirmation: Confirmation | undefined = undefined if (hasIndictmentCaseBeenSubmittedToCourt(theCase.state)) { if (theCase.indictmentHash) { diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts index 98e04d53d4f8..05822ae9a72c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts @@ -4,6 +4,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { CaseController } from '../../case.controller' @@ -19,9 +20,10 @@ describe('CaseController - Get case files record pdf rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(5) + expect(rules).toHaveLength(6) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) + expect(rules).toContain(publicProsecutorStaffRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts index 170857ab4dca..6fee0d26b903 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts @@ -4,6 +4,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { CaseController } from '../../case.controller' @@ -19,9 +20,10 @@ describe('CaseController - Get indictment pdf rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(5) + expect(rules).toHaveLength(6) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) + expect(rules).toContain(publicProsecutorStaffRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) diff --git a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts index f507d84863e2..340d782f767b 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts @@ -37,6 +37,7 @@ import { prisonSystemStaffRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../guards' import { Case, @@ -133,6 +134,7 @@ export class FileController { @RolesRules( prosecutorRule, prosecutorRepresentativeRule, + publicProsecutorStaffRule, districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts index 5455ad7976b0..2d8d88353f35 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts @@ -22,3 +22,5 @@ export const defenderCaseFileCategoriesForIndictmentCases = [ CaseFileCategory.PROSECUTOR_CASE_FILE, CaseFileCategory.DEFENDANT_CASE_FILE, ] + +export const prisonAdminCaseFileCategories = [CaseFileCategory.APPEAL_RULING] diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts index 3526675d6902..a8c2f8295ea7 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts @@ -7,11 +7,10 @@ import { } from '@nestjs/common' import { - CaseFileCategory, isCompletedCase, isDefenceUser, isIndictmentCase, - isPrisonSystemUser, + isPrisonAdminUser, isRequestCase, User, } from '@island.is/judicial-system/types' @@ -21,6 +20,7 @@ import { CaseFile } from '../models/file.model' import { defenderCaseFileCategoriesForIndictmentCases, defenderCaseFileCategoriesForRestrictionAndInvestigationCases, + prisonAdminCaseFileCategories, } from './caseFileCategory' @Injectable() @@ -65,14 +65,13 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { } } - if (isPrisonSystemUser(user)) { - if ( - isCompletedCase(theCase.state) && - caseFile.category && - caseFile.category === CaseFileCategory.APPEAL_RULING - ) { - return true - } + if ( + caseFile.category && + isCompletedCase(theCase.state) && + isPrisonAdminUser(user) && + prisonAdminCaseFileCategories.includes(caseFile.category) + ) { + return true } throw new ForbiddenException(`Forbidden for ${user.role}`) diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts index dd31ac1d7816..e4e7672dc2d6 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts @@ -229,27 +229,19 @@ describe('Limited Access View Case File Guard', () => { describe.each(allowedCaseFileCategories)( 'prison system users can view %s', (category) => { - let thenPrison: Then let thenPrisonAdmin: Then beforeEach(() => { - mockRequest.mockImplementationOnce(() => ({ - user: prisonUser, - case: { type, state }, - caseFile: { category }, - })) mockRequest.mockImplementationOnce(() => ({ user: prisonAdminUser, case: { type, state }, caseFile: { category }, })) - thenPrison = givenWhenThen() thenPrisonAdmin = givenWhenThen() }) it('should activate', () => { - expect(thenPrison.result).toBe(true) expect(thenPrisonAdmin.result).toBe(true) }) }, diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts index d27014f8a5d1..e29e254f3c2d 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts @@ -12,6 +12,7 @@ import { districtCourtRoles, InstitutionType, prosecutionRoles, + publicProsecutorRoles, User, UserRole, } from '@island.is/judicial-system/types' @@ -210,6 +211,59 @@ describe('View Case File Guard', () => { }) }) + describe.each(publicProsecutorRoles)('role %s', (role) => { + describe.each(completedCaseStates)('%s cases', (state) => { + let then: Then + + beforeEach(() => { + mockRequest.mockImplementationOnce(() => ({ + user: { + role, + institution: { + type: InstitutionType.PROSECUTORS_OFFICE, + id: '8f9e2f6d-6a00-4a5e-b39b-95fd110d762e', + }, + }, + case: { state }, + })) + + then = givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + }) + }) + + describe.each( + Object.values(CaseState).filter( + (state) => !completedCaseStates.includes(state), + ), + )('%s cases', (state) => { + let then: Then + + beforeEach(() => { + mockRequest.mockImplementationOnce(() => ({ + user: { + role, + institution: { + type: InstitutionType.PROSECUTORS_OFFICE, + id: '8f9e2f6d-6a00-4a5e-b39b-95fd110d762e', + }, + }, + case: { state }, + })) + + then = givenWhenThen() + }) + + it('should throw ForbiddenException', () => { + expect(then.error).toBeInstanceOf(ForbiddenException) + expect(then.error.message).toBe(`Forbidden for ${role}`) + }) + }) + }) + describe.each(Object.keys(CaseState))('in state %s', (state) => { describe.each( Object.keys(UserRole).filter( diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts index 466cfb67357e..dd00dfcdcbaa 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts @@ -14,6 +14,7 @@ import { isDistrictCourtUser, isPrisonSystemUser, isProsecutionUser, + isPublicProsecutorUser, User, } from '@island.is/judicial-system/types' @@ -44,6 +45,10 @@ export class ViewCaseFileGuard implements CanActivate { return true } + if (isPublicProsecutorUser(user) && isCompletedCase(theCase.state)) { + return true + } + if ( isDistrictCourtUser(user) && ([CaseState.SUBMITTED, CaseState.RECEIVED].includes(theCase.state) || diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts index dd24bcd6d265..86d57470734e 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts @@ -8,6 +8,7 @@ import { prisonSystemStaffRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { FileController } from '../../file.controller' @@ -23,9 +24,10 @@ describe('FileController - Get case file signed url rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(9) + expect(rules).toHaveLength(10) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) + expect(rules).toContain(publicProsecutorStaffRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts index 310e6b9f70c2..fcb2e12d9efd 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts @@ -4,11 +4,7 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { - CaseCompletedGuard, - CaseTypeGuard, - CaseWriteGuard, -} from '../../../case' +import { CaseTypeGuard, CaseWriteGuard } from '../../../case' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' describe('LimitedAccessFileController - Create presigned post guards', () => { diff --git a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts index 2c6aa5278d6d..ebce85337c80 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts @@ -73,10 +73,9 @@ import { } from '../../formatters' import { notifications } from '../../messages' import { type Case, DateLog } from '../case' -import { ExplanatoryComment } from '../case/models/explanatoryComment.model' import { CourtService } from '../court' import { type Defendant, DefendantService } from '../defendant' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { DeliverResponse } from './models/deliver.response' import { Notification, Recipient } from './models/notification.model' import { BaseNotificationService } from './baseNotification.service' diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts index 8a9e78671c95..8103b1106f14 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts @@ -11,7 +11,7 @@ import { type User } from '@island.is/judicial-system/types' import { CaseState, NotificationType } from '@island.is/judicial-system/types' import { type Case } from '../case' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { SendNotificationResponse } from './models/sendNotification.response' @Injectable() diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx index fdb75baa1555..65ec62d224ed 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react' import { - CaseDecision, CaseFileCategory, CaseType, } from '@island.is/judicial-system-web/src/graphql/schema' diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 389df9de9346..57df2e066d8d 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -490,8 +490,8 @@ export const en: TranslatedMessages = { 'edit.phone.inputlabel': 'Phone number', 'edit.phone.button': 'Save', 'edit.phone.button.empty': 'Save empty', - 'edit.phone.button.error': 'Error', - 'edit.phone.button.errorMessage': 'Could not send verification code', + 'edit.phone.error': 'Error', + 'edit.phone.errorMessage': 'Could not send verification code', // edit email 'edit.email.screenTitle': 'Edit Email', @@ -499,8 +499,8 @@ export const en: TranslatedMessages = { 'edit.email.inputlabel': 'Email', 'edit.email.button': 'Save', 'edit.email.button.empty': 'Save empty', - 'edit.email.button.error': 'Error', - 'edit.email.button.errorMessage': 'Could not send verification code', + 'edit.email.error': 'Error', + 'edit.email.errorMessage': 'Could not send verification code', // edit bank info 'edit.bankinfo.screenTitle': 'Edit Bank Info', @@ -510,6 +510,8 @@ export const en: TranslatedMessages = { 'edit.bankinfo.inputlabel.book': 'Hb.', 'edit.bankinfo.inputlabel.number': 'Account number', 'edit.bankinfo.button': 'Save', + 'edit.bankinfo.error': 'Error', + 'edit.bankinfo.errorMessage': 'Could not save bank info', // edit confirm 'edit.confirm.screenTitle': 'Confirm edit', @@ -523,6 +525,8 @@ export const en: TranslatedMessages = { 'edit.confirm.inputlabel': 'Security number', 'edit.cancel.button': 'Cancel', 'edit.confirm.button': 'Confirm', + 'edit.confirm.error': 'Error', + 'edit.confirm.errorMessage': 'Could not update information', // air discount 'airDiscount.screenTitle': 'Air discount scheme', diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index 502f6f0ebc57..3af0414f90f1 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -490,8 +490,8 @@ export const is = { 'edit.phone.inputlabel': 'Símanúmer', 'edit.phone.button': 'Vista', 'edit.phone.button.empty': 'Vista tómt', - 'edit.phone.button.error': 'Villa', - 'edit.phone.button.errorMessage': 'Gat ekki sent staðfestingarkóða', + 'edit.phone.error': 'Villa', + 'edit.phone.errorMessage': 'Gat ekki sent staðfestingarkóða', // edit email 'edit.email.screenTitle': 'Breyta Netfangi', @@ -499,8 +499,8 @@ export const is = { 'edit.email.inputlabel': 'Netfang', 'edit.email.button': 'Vista', 'edit.email.button.empty': 'Vista tómt', - 'edit.email.button.error': 'Villa', - 'edit.email.button.errorMessage': 'Gat ekki sent staðfestingarkóða', + 'edit.email.error': 'Villa', + 'edit.email.errorMessage': 'Gat ekki sent staðfestingarkóða', // edit bank info 'edit.bankinfo.screenTitle': 'Breyta banka upplýsingum', @@ -510,6 +510,8 @@ export const is = { 'edit.bankinfo.inputlabel.book': 'Hb.', 'edit.bankinfo.inputlabel.number': 'Reikningsnúmer', 'edit.bankinfo.button': 'Vista', + 'edit.bankinfo.error': 'Villa', + 'edit.bankinfo.errorMessage': 'Gat ekki vistað reikningsupplýsingar', // edit confirm 'edit.confirm.screenTitle': 'Staðfesta aðgerð', @@ -523,6 +525,8 @@ export const is = { 'edit.confirm.inputlabel': 'Öryggisnúmer', 'edit.cancel.button': 'Hætta við', 'edit.confirm.button': 'Staðfesta', + 'edit.confirm.error': 'Villa', + 'edit.confirm.errorMessage': 'Gat ekki uppfært upplýsingar', // air discount 'airDiscount.screenTitle': 'Loftbrú', diff --git a/apps/native/app/src/screens/inbox/inbox.tsx b/apps/native/app/src/screens/inbox/inbox.tsx index 6b97574ba2cc..6356e3f4c626 100644 --- a/apps/native/app/src/screens/inbox/inbox.tsx +++ b/apps/native/app/src/screens/inbox/inbox.tsx @@ -8,7 +8,6 @@ import { TopLine, InboxCard, } from '@ui' -import { setBadgeCountAsync } from 'expo-notifications' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useIntl } from 'react-intl' import { @@ -371,7 +370,6 @@ export const InboxScreen: NavigationFunctionComponent<{ badgeColor: theme.color.red400, }, }) - setBadgeCountAsync(unreadCount) }, [intl, theme, unreadCount]) const keyExtractor = useCallback((item: ListItem) => { diff --git a/apps/native/app/src/screens/settings/edit-bank-info.tsx b/apps/native/app/src/screens/settings/edit-bank-info.tsx index ea92a495db8e..a6de5f0f03e8 100644 --- a/apps/native/app/src/screens/settings/edit-bank-info.tsx +++ b/apps/native/app/src/screens/settings/edit-bank-info.tsx @@ -119,7 +119,7 @@ export const EditBankInfoScreen: NavigationFunctionComponent = ({ }) if (!res.data) { - throw new Error('Faild to update') + throw new Error('Failed to update') } Navigation.dismissModal(componentId) @@ -128,7 +128,10 @@ export const EditBankInfoScreen: NavigationFunctionComponent = ({ throw new Error('Failed to update') } } catch (e) { - Alert.alert('Villa', 'Gat ekki vistað reikningsupplýsingar') + Alert.alert( + intl.formatMessage({ id: 'edit.bankinfo.error' }), + intl.formatMessage({ id: 'edit.bankinfo.errorMessage' }), + ) } }} /> diff --git a/apps/native/app/src/screens/settings/edit-confirm.tsx b/apps/native/app/src/screens/settings/edit-confirm.tsx index a7e3c22a3527..c624d83e259a 100644 --- a/apps/native/app/src/screens/settings/edit-confirm.tsx +++ b/apps/native/app/src/screens/settings/edit-confirm.tsx @@ -53,10 +53,13 @@ export const EditConfirmScreen: NavigationFunctionComponent = ({ Navigation.dismissModal(parentComponentId) } } else { - throw new Error('Failed') + throw new Error('Failed to update profile') } } catch (e) { - Alert.alert('Villa', 'Gat ekki uppfært upplýsingar') + Alert.alert( + intl.formatMessage({ id: 'edit.confirm.error' }), + intl.formatMessage({ id: 'edit.confirm.errorMessage' }), + ) } setLoading(false) } diff --git a/apps/native/app/src/screens/settings/edit-email.tsx b/apps/native/app/src/screens/settings/edit-email.tsx index b2d5a0df0020..12994c6d11f9 100644 --- a/apps/native/app/src/screens/settings/edit-email.tsx +++ b/apps/native/app/src/screens/settings/edit-email.tsx @@ -100,7 +100,6 @@ export const EditEmailScreen: NavigationFunctionComponent<{ }) if (res.data) { Navigation.dismissModal(componentId) - console.log(res.data, 'Uppfærði tómt netfang') } else { throw new Error('Failed to delete email') } diff --git a/apps/native/app/src/stores/notifications-store.ts b/apps/native/app/src/stores/notifications-store.ts index 768a537b1ab6..37c673adca2e 100644 --- a/apps/native/app/src/stores/notifications-store.ts +++ b/apps/native/app/src/stores/notifications-store.ts @@ -18,6 +18,7 @@ import { } from '../graphql/types/schema' import { ComponentRegistry } from '../utils/component-registry' import { getRightButtons } from '../utils/get-main-root' +import { setBadgeCountAsync } from 'expo-notifications' export interface Notification { id: string @@ -114,6 +115,7 @@ export const notificationsStore = create( }, updateNavigationUnseenCount(unseenCount: number) { set({ unseenCount }) + setBadgeCountAsync(unseenCount) Navigation.mergeOptions(ComponentRegistry.HomeScreen, { topBar: { diff --git a/apps/native/app/src/ui/lib/accordion/accordion-item.tsx b/apps/native/app/src/ui/lib/accordion/accordion-item.tsx index a2792cdaa45f..4305671910e2 100644 --- a/apps/native/app/src/ui/lib/accordion/accordion-item.tsx +++ b/apps/native/app/src/ui/lib/accordion/accordion-item.tsx @@ -98,7 +98,7 @@ export function AccordionItem({ > {icon && {icon}} - {title} + {title} { return ( - + {label} diff --git a/apps/web/components/Organization/MarkdownText/MarkdownText.tsx b/apps/web/components/Organization/MarkdownText/MarkdownText.tsx index d2266de2df6c..a28544a9f807 100644 --- a/apps/web/components/Organization/MarkdownText/MarkdownText.tsx +++ b/apps/web/components/Organization/MarkdownText/MarkdownText.tsx @@ -1,17 +1,29 @@ +import React from 'react' import Markdown from 'markdown-to-jsx' + import { Bullet, BulletList, Text, TextProps } from '@island.is/island-ui/core' -import React from 'react' + import * as styles from './MarkdownText.css' interface MarkdownTextProps { children: string color?: TextProps['color'] variant?: TextProps['variant'] + replaceNewLinesWithBreaks?: boolean } export const MarkdownText: React.FC< React.PropsWithChildren -> = ({ children, color = null, variant = 'default' }) => { +> = ({ + children, + color = null, + variant = 'default', + replaceNewLinesWithBreaks = true, +}) => { + const processedChildren = replaceNewLinesWithBreaks + ? (children as string).replace(/\n/gi, '
') + : children + return (
- {(children as string).replace(/\n/gi, '
')} + {processedChildren}
) diff --git a/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.css.ts b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.css.ts new file mode 100644 index 000000000000..e7006fe6fb85 --- /dev/null +++ b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.css.ts @@ -0,0 +1,7 @@ +import { style } from '@vanilla-extract/css' + +import { theme } from '@island.is/island-ui/theme' + +export const resultBorder = style({ + border: `1px dashed ${theme.color.blue300}`, +}) diff --git a/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx new file mode 100644 index 000000000000..9132ba9f85f6 --- /dev/null +++ b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx @@ -0,0 +1,1089 @@ +import { type PropsWithChildren, useMemo, useRef, useState } from 'react' +import { useIntl } from 'react-intl' +import NumberFormat from 'react-number-format' +import { + parseAsInteger, + parseAsString, + parseAsStringEnum, + useQueryState, +} from 'next-usequerystate' +import { z } from 'zod' + +import { + AlertMessage, + Box, + Button, + GridColumn, + GridRow, + Inline, + Input, + type Option, + RadioButton, + Select, + Stack, + Table, + Text, + Tooltip, +} from '@island.is/island-ui/core' +import { sortAlpha } from '@island.is/shared/utils' +import type { ConnectedComponent } from '@island.is/web/graphql/schema' +import { formatCurrency as formatCurrencyUtil } from '@island.is/web/utils/currency' + +import { MarkdownText } from '../../Organization' +import { translations as t } from './translations.strings' +import * as styles from './ParentalLeaveCalculator.css' + +interface FieldProps { + heading: string + headingTooltip?: string + description?: string + tooltip?: string +} + +const Field = ({ + heading, + headingTooltip, + description, + tooltip, + children, +}: PropsWithChildren) => { + return ( + + + {heading} + {headingTooltip && } + + {description && ( + + {description} + {tooltip && } + + )} + {children} + + ) +} + +enum Status { + PARENTAL_LEAVE = 'parentalLeave', + STUDENT = 'student', + OUTSIDE_WORKFORCE = 'outsideWorkForce', +} + +enum WorkPercentage { + OPTION_1 = 'option1', + OPTION_2 = 'option2', +} + +enum ParentalLeavePeriod { + MONTH = 'month', + THREE_WEEKS = 'threeWeeks', + TWO_WEEKS = 'twoWeeks', +} + +enum Screen { + FORM = 'form', + RESULTS = 'results', +} + +enum LegalDomicileInIceland { + YES = 'y', + NO = 'n', +} + +interface ParentalLeaveCalculatorProps { + slice: ConnectedComponent +} + +interface ScreenProps extends ParentalLeaveCalculatorProps { + changeScreen: () => void +} + +const FormScreen = ({ slice, changeScreen }: ScreenProps) => { + const { formatMessage } = useIntl() + + const statusOptions = useMemo[]>(() => { + return [ + { + label: formatMessage(t.status.parentalLeaveOption), + value: Status.PARENTAL_LEAVE, + }, + { + label: formatMessage(t.status.studentOption), + value: Status.STUDENT, + }, + { + label: formatMessage(t.status.outsideWorkforceOption), + value: Status.OUTSIDE_WORKFORCE, + }, + ] + }, [formatMessage]) + + const yearOptions = useMemo[]>(() => { + const keys = Object.keys(slice.configJson?.yearConfig || {}).map(Number) + keys.sort() + return keys.map((key) => ({ + label: String(key), + value: key, + })) + }, [slice.configJson?.yearConfig]) + + const additionalPensionFundingOptions = useMemo< + Option[] + >(() => { + const options: number[] = slice.configJson + ?.additionalPensionFundingOptions ?? [1, 2, 3, 4] + + return [ + { value: null, label: formatMessage(t.additionalPensionFunding.none) }, + ...options.map((option) => ({ + label: `${option} ${formatMessage( + t.additionalPensionFunding.optionSuffix, + )}`, + value: option, + })), + ] + }, [formatMessage, slice.configJson?.additionalPensionFundingOptions]) + + const unionOptions = useMemo[]>(() => { + const options: { label: string; percentage: number }[] = slice.configJson + ?.unionOptions + ? [...slice.configJson.unionOptions] + : [] + + options.sort(sortAlpha('label')) + + return [ + { + value: null, + label: formatMessage(t.union.none), + }, + ...options.map((option) => ({ + label: option.label, + value: option.label, + })), + ] + }, [formatMessage, slice.configJson?.unionOptions]) + + const parentalLeavePeriodOptions = useMemo[]>(() => { + return [ + { + label: formatMessage(t.parentalLeavePeriod.monthOption), + value: ParentalLeavePeriod.MONTH, + }, + { + label: formatMessage(t.parentalLeavePeriod.threeWeeksOption), + value: ParentalLeavePeriod.THREE_WEEKS, + }, + { + label: formatMessage(t.parentalLeavePeriod.twoWeeksOption), + value: ParentalLeavePeriod.TWO_WEEKS, + }, + ] + }, [formatMessage]) + + const [status, setStatus] = useQueryState( + 'status', + parseAsStringEnum(Object.values(Status)).withDefault(Status.PARENTAL_LEAVE), + ) + const [birthyear, setBirthyear] = useQueryState('birthyear', parseAsInteger) + const [workPercentage, setWorkPercentage] = useQueryState( + 'workPercentage', + parseAsStringEnum(Object.values(WorkPercentage)), + ) + const [income, setIncome] = useQueryState('income', parseAsInteger) + const [ + additionalPensionFundingPercentage, + setAdditionalPensionFundingPercentage, + ] = useQueryState('additionalPensionFunding', parseAsInteger) + const [union, setUnion] = useQueryState('union', parseAsString) + const [personalDiscount, setPersonalDiscount] = useQueryState( + 'personalDiscount', + parseAsInteger.withDefault(100), + ) + const [parentalLeavePeriod, setParentalLeavePeriod] = useQueryState( + 'parentalLeavePeriod', + parseAsStringEnum(Object.values(ParentalLeavePeriod)), + ) + const [parentalLeaveRatio, setParentalLeaveRatio] = useQueryState( + 'parentalLeaveRatio', + parseAsInteger.withDefault(100), + ) + const [legalDomicileInIceland, setLegalDomicileInIceland] = useQueryState( + 'legalDomicileInIceland', + parseAsStringEnum(Object.values(LegalDomicileInIceland)), + ) + + const canCalculate = () => { + let value = + Object.values(Status).includes(status) && + yearOptions.some((year) => year.value === birthyear) + + if (status === Status.OUTSIDE_WORKFORCE) { + value = value && legalDomicileInIceland === LegalDomicileInIceland.YES + } + + if (status === Status.PARENTAL_LEAVE) { + value = + value && + typeof income === 'number' && + income > 0 && + !!workPercentage && + Object.values(WorkPercentage).includes(workPercentage) && + parentalLeavePeriodOptions.some( + (option) => option.value === parentalLeavePeriod, + ) + } + + return value + } + + return ( + + + + { + setBirthyear(option?.value ?? null) + }} + value={yearOptions.find((option) => option.value === birthyear)} + label={formatMessage(t.childBirthYear.label)} + options={yearOptions} + /> + + + {status === Status.PARENTAL_LEAVE && ( + + + + { + setWorkPercentage(WorkPercentage.OPTION_1) + }} + checked={workPercentage === WorkPercentage.OPTION_1} + value={WorkPercentage.OPTION_1} + backgroundColor="white" + large={true} + label={formatMessage(t.workPercentage.option1)} + /> + + + { + setWorkPercentage(WorkPercentage.OPTION_2) + }} + checked={workPercentage === WorkPercentage.OPTION_2} + value={WorkPercentage.OPTION_2} + backgroundColor="white" + large={true} + label={formatMessage(t.workPercentage.option2)} + /> + + + + )} + + {status === Status.PARENTAL_LEAVE && ( + + { + setIncome(Number(value)) + }} + label={formatMessage(t.income.label)} + tooltip={formatMessage(t.income.tooltip)} + value={String(income || '')} + customInput={Input} + name="income" + id="income" + type="text" + inputMode="numeric" + thousandSeparator="." + decimalSeparator="," + suffix={formatMessage(t.income.inputSuffix)} + placeholder={formatMessage(t.income.inputPlaceholder)} + maxLength={ + formatMessage(t.income.inputSuffix).length + + (slice.configJson?.incomeInputMaxLength ?? 12) + } + /> + + )} + + {status === Status.PARENTAL_LEAVE && ( + + { + setUnion(option?.value ?? null) + }} + value={unionOptions.find((option) => option.value === union)} + label={formatMessage(t.union.label)} + options={unionOptions} + /> + + )} + + + { + setPersonalDiscount(Number(value)) + }} + label={formatMessage(t.personalDiscount.label)} + value={String(personalDiscount || '')} + customInput={Input} + name="personalDiscount" + id="personalDiscount" + type="text" + inputMode="numeric" + suffix={formatMessage(t.personalDiscount.suffix)} + placeholder={formatMessage(t.personalDiscount.placeholder)} + format={(value) => { + const maxPersonalDiscount = + slice.configJson?.maxPersonalDiscount ?? 100 + if (Number(value) > maxPersonalDiscount) { + value = String(maxPersonalDiscount) + } + return `${value}${formatMessage(t.personalDiscount.suffix)}` + }} + /> + + + {status === Status.PARENTAL_LEAVE && ( + + { - setDoctorId(val?.value) - }} - /> - - ) : null} - - - - - - - - - - - - ) -} diff --git a/libs/service-portal/health/src/components/RegisterModal/index.ts b/libs/service-portal/health/src/components/RegisterModal/index.ts deleted file mode 100644 index 11dab747046f..000000000000 --- a/libs/service-portal/health/src/components/RegisterModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RegisterModal } from './RegisterModal' diff --git a/libs/service-portal/health/src/screens/DentistRegistration/DentistRegistration.tsx b/libs/service-portal/health/src/screens/DentistRegistration/DentistRegistration.tsx index 8fc188331fd3..bcb7e79f7ed5 100644 --- a/libs/service-portal/health/src/screens/DentistRegistration/DentistRegistration.tsx +++ b/libs/service-portal/health/src/screens/DentistRegistration/DentistRegistration.tsx @@ -13,7 +13,7 @@ import { useGetPaginatedDentistsQuery, useRegisterDentistMutation, } from './DentistRegistration.generated' -import { m } from '@island.is/service-portal/core' +import { Modal, m } from '@island.is/service-portal/core' import { IntroHeader } from '@island.is/portals/core' import { useLocale, useNamespaces } from '@island.is/localization' import { messages } from '../../lib/messages' @@ -22,7 +22,6 @@ import { useDebounce } from 'react-use' import { useNavigate } from 'react-router-dom' import { HealthPaths } from '../../lib/paths' import { RightsPortalDentist } from '@island.is/api/schema' -import { RegisterModal } from '../../components/RegisterModal' import * as styles from './DentistRegistration.css' import { Problem } from '@island.is/react-spa/shared' @@ -40,6 +39,7 @@ export const DentistRegistration = () => { const [activeSearch, setActiveSearch] = useState('') const [selectedDentist, setSelectedDentist] = useState(null) + const [modalVisible, setModalVisible] = useState(false) const [hoverId, setHoverId] = useState(0) const [errorTransfering, setErrorTransfering] = useState(false) const errorBoxRef = useRef(null) @@ -158,30 +158,6 @@ export const DentistRegistration = () => { backgroundColor="blue" /> - - setSelectedDentist(null)} - onAccept={() => { - setErrorTransfering(false) - if (selectedDentist && selectedDentist.id) { - registerDentist({ - variables: { - input: { - id: `${selectedDentist.id}`, - }, - }, - }) - } - }} - id={'dentistRegisterModal'} - title={`${formatMessage(messages.dentistModalTitle)} ${ - selectedDentist?.name - }`} - description="" - isVisible={!!selectedDentist} - buttonLoading={loadingTranser} - /> - {loading ? ( ) : ( @@ -219,19 +195,69 @@ export const DentistRegistration = () => { visible: dentist.id === hoverId, })} > - + title={`${formatMessage( + messages.dentistModalTitle, + )} ${selectedDentist?.name}`} + buttons={[ + { + id: 'RegisterModalAccept', + type: 'primary' as const, + text: formatMessage( + messages.healthRegisterModalAccept, + ), + onClick: () => { + setErrorTransfering(false) + setModalVisible(false) + if (selectedDentist && selectedDentist.id) { + registerDentist({ + variables: { + input: { + id: `${selectedDentist.id}`, + }, + }, + }) + } + }, + }, + { + id: 'RegisterModalDecline', + type: 'ghost' as const, + text: formatMessage( + messages.healthRegisterModalDecline, + ), + onClick: () => { + setModalVisible(false) + }, + }, + ]} + disclosure={ + + } + /> ) : undefined} diff --git a/libs/service-portal/health/src/screens/HealthCenterRegistration/HealthCenterRegistration.tsx b/libs/service-portal/health/src/screens/HealthCenterRegistration/HealthCenterRegistration.tsx index 7e84491a1e2d..432eddfdccf4 100644 --- a/libs/service-portal/health/src/screens/HealthCenterRegistration/HealthCenterRegistration.tsx +++ b/libs/service-portal/health/src/screens/HealthCenterRegistration/HealthCenterRegistration.tsx @@ -16,6 +16,7 @@ import { EmptyState, ErrorScreen, ExcludesFalse, + Modal, } from '@island.is/service-portal/core' import { messages } from '../../lib/messages' import * as styles from './HealthRegistration.css' @@ -26,7 +27,6 @@ import { RightsPortalHealthCenter } from '@island.is/api/schema' import { useNavigate } from 'react-router-dom' import { HealthPaths } from '../../lib/paths' import { formatHealthCenterName } from '../../utils/format' -import { RegisterModal } from '../../components/RegisterModal' import { useGetHealthCenterDoctorsLazyQuery, useGetHealthCenterQuery, @@ -237,22 +237,6 @@ const HealthCenterRegistration = () => { )} - { - setSelectedHealthCenter(null) - setHealthCenterDoctors([]) - }} - onAccept={handleHealthCenterTransfer} - isVisible={!!selectedHealthCenter} - buttonLoading={loadingTransfer} - healthCenterDoctors={healthCenterDoctors} - /> - setFilter(val)} @@ -306,21 +290,61 @@ const HealthCenterRegistration = () => { visible: healthCenter.id === hoverId, })} > - + buttons={[ + { + id: 'RegisterHealthCenterModalAccept', + type: 'primary' as const, + text: formatMessage( + messages.healthRegisterModalAccept, + ), + onClick: handleHealthCenterTransfer, + }, + { + id: 'RegisterHealthCenterModalDecline', + type: 'ghost' as const, + text: formatMessage( + messages.healthRegisterModalDecline, + ), + onClick: () => { + setSelectedHealthCenter(null) + setHealthCenterDoctors([]) + }, + }, + ]} + disclosure={ + + } + />