diff --git a/apps/application-system/api/src/app/modules/application/application.controller.ts b/apps/application-system/api/src/app/modules/application/application.controller.ts index 8440e256c614..3e70189aa46e 100644 --- a/apps/application-system/api/src/app/modules/application/application.controller.ts +++ b/apps/application-system/api/src/app/modules/application/application.controller.ts @@ -76,7 +76,10 @@ import { ApplicationResponseDto } from './dto/application.response.dto' import { PresignedUrlResponseDto } from './dto/presignedUrl.response.dto' import { AssignApplicationDto } from './dto/assignApplication.dto' import { verifyToken } from './utils/tokenUtils' -import { getApplicationLifecycle } from './utils/application' +import { + getApplicationLifecycle, + removeObjectWithKeyFromAnswers, +} from './utils/application' import { DecodedAssignmentToken } from './types' import { ApplicationAccessService } from './tools/applicationAccess.service' import { CurrentLocale } from './utils/currentLocale' @@ -870,6 +873,10 @@ export class ApplicationController { existingApplication.id, { attachments: omit(existingApplication.attachments, key), + answers: removeObjectWithKeyFromAnswers( + existingApplication.answers, + key, + ), }, ) diff --git a/apps/application-system/api/src/app/modules/application/utils/application.ts b/apps/application-system/api/src/app/modules/application/utils/application.ts index b9f1cc3d2cfa..cbaed22ec0b4 100644 --- a/apps/application-system/api/src/app/modules/application/utils/application.ts +++ b/apps/application-system/api/src/app/modules/application/utils/application.ts @@ -156,3 +156,105 @@ export const mockApplicationFromTypeId = ( status: ApplicationStatus.IN_PROGRESS, } } + +type RecordType = Record + +const MAX_DEPTH = 100 + +export const removeObjectWithKeyFromAnswers = ( + answers: object, + keyToRemove: string, + depth = 0, +): object => { + if (depth >= MAX_DEPTH) { + console.warn( + 'Maximum recursion depth reached while calling removeObjectWithKeyFromAnswers', + ) + return answers + } + // Handle arrays + if (Array.isArray(answers)) { + return cleanArray(answers, keyToRemove, depth) + } + + // Handle objects + if (isValidObject(answers)) { + return cleanObject(answers, keyToRemove, depth) + } + + return answers +} + +const cleanArray = ( + array: unknown[], + keyToRemove: string, + depth: number, +): unknown[] => { + const filteredArray = array.filter((item) => { + if (isValidObject(item)) { + return !containsKey(item, keyToRemove) + } + return item !== keyToRemove + }) + + return filteredArray.map((item) => { + if (isObject(item)) { + return removeObjectWithKeyFromAnswers(item, keyToRemove, depth + 1) + } + return item + }) +} + +const cleanObject = ( + obj: object, + keyToRemove: string, + depth: number, +): RecordType => { + return Object.entries(obj).reduce((acc, [field, value]) => { + if (isValidObject(value)) { + if (!Array.isArray(value) && containsKey(value, keyToRemove)) { + return acc + } + + const cleanedValue = removeObjectWithKeyFromAnswers( + value, + keyToRemove, + depth + 1, + ) + + // For arrays or objects with content, keep them + if (hasContent(cleanedValue)) { + acc[field] = cleanedValue + } + // Special case: keep empty arrays + else if (Array.isArray(value)) { + acc[field] = cleanedValue + } + return acc + } + + // Handle primitive values + if (value !== keyToRemove) { + acc[field] = value + } + return acc + }, {}) +} + +const isValidObject = (value: unknown): value is object => { + return value !== null && typeof value === 'object' +} + +const containsKey = (obj: object, key: string): boolean => { + return Object.values(obj).includes(key) +} + +const hasContent = (value: unknown): boolean => { + if (Array.isArray(value)) { + return value.length > 0 + } + if (isValidObject(value)) { + return Object.keys(value).length > 0 + } + return false +} diff --git a/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts b/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts index 2eafaf9a4fc3..1a17720b751b 100644 --- a/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts +++ b/apps/application-system/api/src/app/modules/application/utils/applicationUtils.spec.ts @@ -3,6 +3,7 @@ import { getApplicationNameTranslationString, getApplicationStatisticsNameTranslationString, getPaymentStatusForAdmin, + removeObjectWithKeyFromAnswers, } from './application' import { createApplication, @@ -196,4 +197,208 @@ describe('Testing utility functions for applications', () => { }) }) }) + + describe('removeAttachmentFromAnswers', () => { + it('Should remove an object from an array that contains the given key and leave the array empty', () => { + const givenAnswers = { + documents: [ + { id: 'doc1', attachmentId: 'some-key-123', name: 'Document 1' }, + ], + } + const expectedAnswers = { + documents: [], + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should remove nested objects that contain the given key', () => { + const givenAnswers = { + section1: { + attachment: { id: 'some-key-123', name: 'Remove me' }, + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + const expectedAnswers = { + section1: { + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should remove an object from an array that contains the given key', () => { + const givenAnswers = { + documents: [ + { id: 'doc1', attachmentId: 'some-key-123', name: 'Document 1' }, + { id: 'doc2', attachmentId: 'keep-this-key', name: 'Document 2' }, + ], + } + const expectedAnswers = { + documents: [ + { id: 'doc2', attachmentId: 'keep-this-key', name: 'Document 2' }, + ], + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should remove nested objects that contain the given key', () => { + const givenAnswers = { + section1: { + attachment: { id: 'some-key-123', name: 'Remove me' }, + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + const expectedAnswers = { + section1: { + otherData: 'keep this', + }, + section2: { + data: 'keep this too', + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should handle deeply nested arrays and objects', () => { + const givenAnswers = { + deepSection: { + documents: [ + { + files: [ + { id: 'file1', attachmentId: 'some-key-123' }, + { id: 'file2', attachmentId: 'keep-this' }, + ], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + }, + } + const expectedAnswers = { + deepSection: { + documents: [ + { + files: [{ id: 'file2', attachmentId: 'keep-this' }], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should handle even more complex deeply nested arrays and objects', () => { + const givenAnswers = { + deepSection: { + someRandomProp: { data: 'Some data' }, + deeperSection: { + documents: [ + { + files: [ + { id: 'file1', attachmentId: 'some-key-123', nr: 77 }, + { id: 'file2', attachmentId: 'keep-this', nr: 55 }, + ], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + otherSection: { + nr: 100, + name: 'Some Name', + kids: [ + { kid: 'Some kid', phone: 1234567 }, + { kid: 'Some other kid', phone: 1234568 }, + ], + }, + }, + }, + } + + const expectedAnswers = { + deepSection: { + someRandomProp: { data: 'Some data' }, + deeperSection: { + documents: [ + { + files: [{ id: 'file2', attachmentId: 'keep-this', nr: 55 }], + }, + { + files: [{ id: 'file3', attachmentId: 'also-keep-this' }], + }, + ], + otherSection: { + nr: 100, + name: 'Some Name', + kids: [ + { kid: 'Some kid', phone: 1234567 }, + { kid: 'Some other kid', phone: 1234568 }, + ], + }, + }, + }, + } + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + + it('Should return empty object when no answers provided', () => { + const givenAnswers = {} + const expectedAnswers = {} + + const result = removeObjectWithKeyFromAnswers( + givenAnswers, + 'some-key-123', + ) + + expect(result).toEqual(expectedAnswers) + }) + }) }) diff --git a/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts b/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts index 325210f11a3a..d25bad9175cb 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts @@ -109,4 +109,9 @@ export class UpdateDefendantInput { @IsOptional() @Field(() => Boolean, { nullable: true }) readonly caseFilesSharedWithDefender?: boolean + + @Allow() + @IsOptional() + @Field(() => Boolean, { nullable: true }) + readonly isSentToPrisonAdmin?: boolean } 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 4f641c32efe8..b5276f9bc104 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 @@ -101,4 +101,10 @@ export class Defendant { @Field(() => Boolean, { nullable: true }) readonly caseFilesSharedWithDefender?: boolean + + @Field(() => Boolean, { nullable: true }) + readonly isSentToPrisonAdmin?: boolean + + @Field(() => String, { nullable: true }) + readonly sentToPrisonAdminDate?: string } diff --git a/apps/judicial-system/backend/migrations/20241112125245-update-defendant.js b/apps/judicial-system/backend/migrations/20241112125245-update-defendant.js new file mode 100644 index 000000000000..f10aab86673c --- /dev/null +++ b/apps/judicial-system/backend/migrations/20241112125245-update-defendant.js @@ -0,0 +1,25 @@ +'use strict' + +module.exports = { + up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.addColumn( + 'defendant', + 'is_sent_to_prison_admin', + { + type: Sequelize.BOOLEAN, + allowNull: true, + }, + { transaction }, + ), + ) + }, + + down(queryInterface) { + return queryInterface.sequelize.transaction((transaction) => + queryInterface.removeColumn('defendant', 'is_sent_to_prison_admin', { + transaction, + }), + ) + }, +} diff --git a/apps/judicial-system/backend/migrations/20241114123528-create-defendant-event-log.js b/apps/judicial-system/backend/migrations/20241114123528-create-defendant-event-log.js new file mode 100644 index 000000000000..3903be153082 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20241114123528-create-defendant-event-log.js @@ -0,0 +1,56 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.createTable( + 'defendant_event_log', + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4, + }, + created: { + type: 'TIMESTAMP WITH TIME ZONE', + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + allowNull: false, + }, + modified: { + type: 'TIMESTAMP WITH TIME ZONE', + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + allowNull: false, + }, + case_id: { + type: Sequelize.UUID, + references: { + model: 'case', + key: 'id', + }, + allowNull: false, + }, + defendant_id: { + type: Sequelize.UUID, + references: { + model: 'defendant', + key: 'id', + }, + allowNull: false, + }, + event_type: { + type: Sequelize.STRING, + allowNull: false, + }, + }, + { transaction: t }, + ), + ) + }, + + down: (queryInterface) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.dropTable('defendant_event_log', { transaction: t }), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.ts b/apps/judicial-system/backend/src/app/formatters/formatters.ts index 552a328fc515..0f43edf7200b 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.ts @@ -5,7 +5,6 @@ import { DEFENDER_ROUTE, } from '@island.is/judicial-system/consts' import { - capitalize, enumerate, formatCaseType, formatDate, @@ -14,11 +13,7 @@ import { laws, readableIndictmentSubtypes, } from '@island.is/judicial-system/formatters' -import { - AdvocateType, - Gender, - UserRole, -} from '@island.is/judicial-system/types' +import { Gender, UserRole } from '@island.is/judicial-system/types' import { CaseCustodyRestrictions, CaseLegalProvisions, 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 df2e7ee8dc61..5cff475da8fd 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 @@ -40,6 +40,7 @@ import { CaseType, DateType, dateTypes, + defendantEventTypes, EventType, eventTypes, isCompletedCase, @@ -59,7 +60,12 @@ import { } from '../../formatters' import { AwsS3Service } from '../aws-s3' import { CourtService } from '../court' -import { CivilClaimant, Defendant, DefendantService } from '../defendant' +import { + CivilClaimant, + Defendant, + DefendantEventLog, + DefendantService, +} from '../defendant' import { EventService } from '../event' import { EventLog, EventLogService } from '../event-log' import { CaseFile, FileService } from '../file' @@ -285,6 +291,14 @@ export const include: Includeable[] = [ order: [['created', 'DESC']], separate: true, }, + { + model: DefendantEventLog, + as: 'eventLogs', + required: false, + where: { eventType: defendantEventTypes }, + order: [['created', 'DESC']], + separate: true, + }, ], separate: true, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts index e38b445c792b..e4200b4dd5ee 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts @@ -6,7 +6,6 @@ import { CaseState, CaseType, EventType, - getIndictmentVerdictAppealDeadlineStatus, IndictmentCaseReviewDecision, isCourtOfAppealsUser, isDefenceUser, @@ -290,16 +289,10 @@ const canPrisonAdminUserAccessCase = ( return false } - // Check defendant verdict appeal deadline access - const canAppealVerdict = true - const verdictInfo = (theCase.defendants || []).map< - [boolean, Date | undefined] - >((defendant) => [canAppealVerdict, defendant.verdictViewDate]) - - const [_, indictmentVerdictAppealDeadlineExpired] = - getIndictmentVerdictAppealDeadlineStatus(verdictInfo) - - if (!indictmentVerdictAppealDeadlineExpired) { + // Check if a defendant has been sent to the prison admin + if ( + !theCase.defendants?.some((defendant) => defendant.isSentToPrisonAdmin) + ) { return false } } diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts index e4bf7c5af74d..34ae100c239d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts @@ -25,7 +25,6 @@ import { RequestSharedWithDefender, restrictionCases, UserRole, - VERDICT_APPEAL_WINDOW_DAYS, } from '@island.is/judicial-system/types' const getProsecutionUserCasesQueryFilter = (user: User): WhereOptions => { @@ -215,12 +214,10 @@ const getPrisonAdminUserCasesQueryFilter = (): WhereOptions => { indictment_ruling_decision: CaseIndictmentRulingDecision.RULING, indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT, id: { - [Op.notIn]: Sequelize.literal(` + [Op.in]: Sequelize.literal(` (SELECT case_id FROM defendant - WHERE (verdict_appeal_date IS NOT NULL - OR verdict_view_date IS NULL - OR verdict_view_date > NOW() - INTERVAL '${VERDICT_APPEAL_WINDOW_DAYS} days')) + WHERE is_sent_to_prison_admin = true) `), }, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts index eb3cc40db316..cb6a69ebaf7c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts @@ -21,7 +21,6 @@ import { RequestSharedWithDefender, restrictionCases, UserRole, - VERDICT_APPEAL_WINDOW_DAYS, } from '@island.is/judicial-system/types' import { getCasesQueryFilter } from '../cases.filter' @@ -392,12 +391,10 @@ describe('getCasesQueryFilter', () => { indictment_ruling_decision: CaseIndictmentRulingDecision.RULING, indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT, id: { - [Op.notIn]: Sequelize.literal(` + [Op.in]: Sequelize.literal(` (SELECT case_id FROM defendant - WHERE (verdict_appeal_date IS NOT NULL - OR verdict_view_date IS NULL - OR verdict_view_date > NOW() - INTERVAL '${VERDICT_APPEAL_WINDOW_DAYS} days')) + WHERE is_sent_to_prison_admin = true) `), }, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts index de0e36f943a2..570de65e3f81 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/prisonAdminUserFilter.spec.ts @@ -191,14 +191,29 @@ describe.each(prisonSystemRoles)('prison admin user %s', (role) => { describe.each(accessibleIndictmentCaseReviewDecisions)( 'accessible indictment case review decision %s', (indictmentReviewDecision) => { - const theCase = { - type, - state, - indictmentRulingDecision, - indictmentReviewDecision, - } as Case + describe('no defendant has been sent to the prison admin', () => { + const theCase = { + type, + state, + indictmentRulingDecision, + indictmentReviewDecision, + defendants: [{}], + } as Case + + verifyNoAccess(theCase, user) + }) + + describe('a defendant has been sent to the prison admin', () => { + const theCase = { + type, + state, + indictmentRulingDecision, + indictmentReviewDecision, + defendants: [{ isSentToPrisonAdmin: true }], + } as Case - verifyReadAccess(theCase, user) + verifyReadAccess(theCase, 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 c22e624b71a1..85e302ad6754 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,12 +7,23 @@ import { NestInterceptor, } from '@nestjs/common' +import { Defendant, DefendantEventLog } from '../../defendant' import { Case } from '../models/case.model' import { CaseString } from '../models/caseString.model' +export const transformDefendants = (defendants?: Defendant[]) => { + return defendants?.map((defendant) => ({ + ...defendant.toJSON(), + sentToPrisonAdminDate: defendant.isSentToPrisonAdmin + ? DefendantEventLog.sentToPrisonAdminDate(defendant.eventLogs)?.created + : undefined, + })) +} + const transformCase = (theCase: Case) => { return { ...theCase.toJSON(), + defendants: transformDefendants(theCase.defendants), postponedIndefinitelyExplanation: CaseString.postponedIndefinitelyExplanation(theCase.caseStrings), civilDemands: CaseString.civilDemands(theCase.caseStrings), diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts index 3044fdff06de..dbc56797bcb1 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseList.interceptor.ts @@ -12,6 +12,7 @@ import { IndictmentDecision } from '@island.is/judicial-system/types' import { Case } from '../models/case.model' import { CaseString } from '../models/caseString.model' import { DateLog } from '../models/dateLog.model' +import { transformDefendants } from './case.interceptor' @Injectable() export class CaseListInterceptor implements NestInterceptor { @@ -29,7 +30,7 @@ export class CaseListInterceptor implements NestInterceptor { policeCaseNumbers: theCase.policeCaseNumbers, state: theCase.state, type: theCase.type, - defendants: theCase.defendants, + defendants: transformDefendants(theCase.defendants), courtCaseNumber: theCase.courtCaseNumber, decision: theCase.decision, validToDate: theCase.validToDate, diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index bd17a87781d9..14e595b23ff0 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -23,6 +23,7 @@ import { CaseNotificationType, CaseState, dateTypes, + defendantEventTypes, eventTypes, stringTypes, UserRole, @@ -34,6 +35,7 @@ import { CivilClaimant, CivilClaimantService, Defendant, + DefendantEventLog, DefendantService, } from '../defendant' import { EventLog } from '../event-log' @@ -182,6 +184,14 @@ export const include: Includeable[] = [ order: [['created', 'DESC']], separate: true, }, + { + model: DefendantEventLog, + as: 'eventLogs', + required: false, + where: { eventType: defendantEventTypes }, + order: [['created', 'DESC']], + separate: true, + }, ], separate: true, }, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts index 64517bda3c3a..3aa77ff57b8f 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts @@ -7,6 +7,7 @@ import { CaseModule } from '../case/case.module' import { CourtModule } from '../court/court.module' import { CivilClaimant } from './models/civilClaimant.model' import { Defendant } from './models/defendant.model' +import { DefendantEventLog } from './models/defendantEventLog.model' import { CivilClaimantController } from './civilClaimant.controller' import { CivilClaimantService } from './civilClaimant.service' import { DefendantController } from './defendant.controller' @@ -18,7 +19,7 @@ import { InternalDefendantController } from './internalDefendant.controller' MessageModule, forwardRef(() => CourtModule), forwardRef(() => CaseModule), - SequelizeModule.forFeature([Defendant, CivilClaimant]), + SequelizeModule.forFeature([Defendant, CivilClaimant, DefendantEventLog]), ], controllers: [ DefendantController, 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 b266c32f2a76..e163917b953f 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 @@ -22,6 +22,7 @@ import { CaseNotificationType, CaseState, CaseType, + DefendantEventType, DefendantNotificationType, DefenderChoice, isIndictmentCase, @@ -33,12 +34,15 @@ import { CreateDefendantDto } from './dto/createDefendant.dto' import { InternalUpdateDefendantDto } from './dto/internalUpdateDefendant.dto' import { UpdateDefendantDto } from './dto/updateDefendant.dto' import { Defendant } from './models/defendant.model' +import { DefendantEventLog } from './models/defendantEventLog.model' import { DeliverResponse } from './models/deliver.response' @Injectable() export class DefendantService { constructor( @InjectModel(Defendant) private readonly defendantModel: typeof Defendant, + @InjectModel(DefendantEventLog) + private readonly defendantEventLogModel: typeof DefendantEventLog, private readonly courtService: CourtService, private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, @@ -258,6 +262,14 @@ export class DefendantService { update, ) + if (update.isSentToPrisonAdmin) { + this.defendantEventLogModel.create({ + caseId: theCase.id, + defendantId: defendant.id, + eventType: DefendantEventType.SENT_TO_PRISON_ADMIN, + }) + } + await this.sendIndictmentCaseUpdateDefendantMessages( theCase, updatedDefendant, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts index c59f91dcaa0d..1082d70615e1 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts @@ -135,4 +135,9 @@ export class UpdateDefendantDto { @IsBoolean() @ApiPropertyOptional({ type: Boolean }) readonly caseFilesSharedWithDefender?: boolean + + @IsOptional() + @IsBoolean() + @ApiPropertyOptional({ type: Boolean }) + readonly isSentToPrisonAdmin?: boolean } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/index.ts b/apps/judicial-system/backend/src/app/modules/defendant/index.ts index 5fb8a9c29896..6b6c33d19c71 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/index.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/index.ts @@ -2,6 +2,7 @@ export { Defendant } from './models/defendant.model' export { DefendantService } from './defendant.service' export { DefendantExistsGuard } from './guards/defendantExists.guard' export { CurrentDefendant } from './guards/defendant.decorator' +export { DefendantEventLog } from './models/defendantEventLog.model' export { CivilClaimant } from './models/civilClaimant.model' export { CivilClaimantService } from './civilClaimant.service' diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts index be827e0468b0..bc31334d6bdd 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts @@ -23,6 +23,7 @@ import { import { Case } from '../../case/models/case.model' import { Subpoena } from '../../subpoena/models/subpoena.model' +import { DefendantEventLog } from './defendantEventLog.model' @Table({ tableName: 'defendant', @@ -195,4 +196,12 @@ export class Defendant extends Model { @Column({ type: DataType.BOOLEAN, allowNull: true }) @ApiPropertyOptional({ type: Boolean }) caseFilesSharedWithDefender?: boolean + + @Column({ type: DataType.BOOLEAN, allowNull: true }) + @ApiPropertyOptional({ type: Boolean }) + isSentToPrisonAdmin?: boolean + + @HasMany(() => DefendantEventLog, { foreignKey: 'defendantId' }) + @ApiPropertyOptional({ type: () => DefendantEventLog, isArray: true }) + eventLogs?: DefendantEventLog[] } 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 new file mode 100644 index 000000000000..ca0a332704ee --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendantEventLog.model.ts @@ -0,0 +1,64 @@ +import { + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + Table, + UpdatedAt, +} from 'sequelize-typescript' + +import { ApiProperty } from '@nestjs/swagger' + +import { DefendantEventType } from '@island.is/judicial-system/types' + +import { Case } from '../../case/models/case.model' +import { Defendant } from './defendant.model' + +@Table({ + tableName: 'defendant_event_log', + timestamps: true, +}) +export class DefendantEventLog extends Model { + static sentToPrisonAdminDate(defendantEventLogs?: DefendantEventLog[]) { + return defendantEventLogs?.find( + (defendantEventLog) => + defendantEventLog.eventType === DefendantEventType.SENT_TO_PRISON_ADMIN, + ) + } + + @Column({ + type: DataType.UUID, + primaryKey: true, + allowNull: false, + defaultValue: DataType.UUIDV4, + }) + @ApiProperty({ type: String }) + id!: string + + @CreatedAt + @ApiProperty({ type: Date }) + created!: Date + + @UpdatedAt + @ApiProperty({ type: Date }) + modified!: Date + + @ForeignKey(() => Case) + @Column({ type: DataType.UUID, allowNull: false }) + @ApiProperty({ type: String }) + caseId!: string + + @ForeignKey(() => Defendant) + @Column({ type: DataType.UUID, allowNull: false }) + @ApiProperty({ type: String }) + defendantId!: string + + @Column({ + type: DataType.ENUM, + allowNull: false, + values: Object.values(DefendantEventType), + }) + @ApiProperty({ enum: DefendantEventType }) + eventType!: DefendantEventType +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts index d8661906faa4..10eba40190cc 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts @@ -22,12 +22,6 @@ type GivenWhenThen = ( describe('CivilClaimantController - Update', () => { const caseId = uuid() const civilClaimantId = uuid() - const civilClaimaint = { - id: civilClaimantId, - caseId, - nationalId: uuid(), - name: 'Original Name', - } as CivilClaimant let mockMessageService: MessageService let mockCivilClaimantModel: typeof CivilClaimant diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts index 7ca4abf0ae2d..e6ef10b79545 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts @@ -20,6 +20,7 @@ import { DefendantService } from '../defendant.service' import { InternalDefendantController } from '../internalDefendant.controller' import { CivilClaimant } from '../models/civilClaimant.model' import { Defendant } from '../models/defendant.model' +import { DefendantEventLog } from '../models/defendantEventLog.model' jest.mock('@island.is/judicial-system/message') jest.mock('../../user/user.service') @@ -70,6 +71,17 @@ export const createTestingDefendantModule = async () => { findByPk: jest.fn(), }, }, + { + provide: getModelToken(DefendantEventLog), + useValue: { + findOne: jest.fn(), + findAll: jest.fn(), + create: jest.fn(), + update: jest.fn(), + destroy: jest.fn(), + findByPk: jest.fn(), + }, + }, DefendantService, CivilClaimantService, ], diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts index ea850b4407f1..8ad9cc1ec040 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/internalDefendantController/internalDefendantControllerGuards.spec.ts @@ -3,7 +3,6 @@ import { CanActivate } from '@nestjs/common' import { TokenGuard } from '@island.is/judicial-system/auth' import { CaseExistsGuard } from '../../../case' -import { DefendantExistsGuard } from '../../guards/defendantExists.guard' import { InternalDefendantController } from '../../internalDefendant.controller' describe('InternalDefendantController - guards', () => { 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 8e0a7940bc95..54626a9f5700 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 @@ -81,6 +81,7 @@ export class FileController { courtOfAppealsJudgeRule, courtOfAppealsRegistrarRule, courtOfAppealsAssistantRule, + publicProsecutorStaffRule, ) @Post('file/url') @ApiCreatedResponse({ @@ -107,6 +108,7 @@ export class FileController { courtOfAppealsJudgeRule, courtOfAppealsRegistrarRule, courtOfAppealsAssistantRule, + publicProsecutorStaffRule, ) @Post('file') @ApiCreatedResponse({ diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts index ddd88b1ea428..1321f5423d70 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createCaseFileRolesRules.spec.ts @@ -7,6 +7,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { FileController } from '../../file.controller' @@ -22,7 +23,7 @@ describe('FileController - Create case file rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(8) + expect(rules).toHaveLength(9) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) expect(rules).toContain(districtCourtJudgeRule) @@ -31,5 +32,6 @@ describe('FileController - Create case file rules', () => { expect(rules).toContain(courtOfAppealsJudgeRule) expect(rules).toContain(courtOfAppealsRegistrarRule) expect(rules).toContain(courtOfAppealsAssistantRule) + expect(rules).toContain(publicProsecutorStaffRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts index 46c5c1a7f766..04768bb581f7 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/createPresignedPostRolesRules.spec.ts @@ -7,6 +7,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { FileController } from '../../file.controller' @@ -22,7 +23,7 @@ describe('FileController - Create presigned post rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(8) + expect(rules).toHaveLength(9) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) expect(rules).toContain(districtCourtJudgeRule) @@ -31,5 +32,6 @@ describe('FileController - Create presigned post rules', () => { expect(rules).toContain(courtOfAppealsJudgeRule) expect(rules).toContain(courtOfAppealsRegistrarRule) expect(rules).toContain(courtOfAppealsAssistantRule) + expect(rules).toContain(publicProsecutorStaffRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts index 5f47cbac54c6..910270ff53e7 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts @@ -30,7 +30,6 @@ import { lowercase, } from '@island.is/judicial-system/formatters' import { - AdvocateType, CaseAppealRulingDecision, CaseCustodyRestrictions, CaseDecision, diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts index fe4c450c5873..8c89e945896c 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts @@ -4,7 +4,7 @@ import { uuid } from 'uuidv4' import { getModelToken } from '@nestjs/sequelize' import { Test } from '@nestjs/testing' -import { FormatMessage, IntlService } from '@island.is/cms-translations' +import { IntlService } from '@island.is/cms-translations' import { createTestIntl } from '@island.is/cms-translations/test' import { EmailService } from '@island.is/email-service' import { LOGGER_PROVIDER } from '@island.is/logging' diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts index 2ff1ebb316c3..c8f120ffbf71 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts @@ -3,10 +3,7 @@ import { uuid } from 'uuidv4' import { EmailService } from '@island.is/email-service' import { ConfigType } from '@island.is/nest/config' -import { - DEFENDER_INDICTMENT_ROUTE, - DEFENDER_ROUTE, -} from '@island.is/judicial-system/consts' +import { DEFENDER_ROUTE } from '@island.is/judicial-system/consts' import { CaseNotificationType, CaseType, diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts index 6959fde99979..3effe645d714 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendRulingNotifications.spec.ts @@ -140,8 +140,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { it('should send email to prosecutor', () => { const expectedLink = `` expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) - expect(mockEmailService.sendEmail).toHaveBeenNthCalledWith( - 2, + expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: [{ name: prosecutor.name, address: prosecutor.email }], subject: 'Úrskurður í máli 007-2022-07', @@ -174,8 +173,7 @@ describe('InternalNotificationController - Send ruling notifications', () => { it('should send email to prosecutor', () => { const expectedLink = `` expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(2) - expect(mockEmailService.sendEmail).toHaveBeenNthCalledWith( - 2, + expect(mockEmailService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: [{ name: testProsecutor.name, address: testProsecutor.email }], subject: 'Úrskurður í máli 007-2022-07 leiðréttur', 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 64fc48de8dc9..5c9bea446004 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 @@ -1,5 +1,5 @@ import { Base64 } from 'js-base64' -import { Includeable, Sequelize } from 'sequelize' +import { Includeable } from 'sequelize' import { Transaction } from 'sequelize/types' import { @@ -8,7 +8,7 @@ import { Injectable, InternalServerErrorException, } from '@nestjs/common' -import { InjectConnection, InjectModel } from '@nestjs/sequelize' +import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' diff --git a/apps/judicial-system/web/messages/Core/errors.ts b/apps/judicial-system/web/messages/Core/errors.ts index 1333b3f2484d..b2388cfda63e 100644 --- a/apps/judicial-system/web/messages/Core/errors.ts +++ b/apps/judicial-system/web/messages/Core/errors.ts @@ -162,4 +162,10 @@ export const errors = defineMessages({ defaultMessage: 'Dagsetning ekki rétt slegin inn', description: 'Notaður sem villuskilaboð þegar dagsetning er ekki löggild', }, + uploadFailed: { + id: 'judicial.system.core:errors.upload_failed', + defaultMessage: 'Upp kom villa við að hlaða upp skjali', + description: + 'Notaður sem villuskilaboð þegar ekki tekst að hlaða upp skjali', + }, }) diff --git a/apps/judicial-system/web/pages/rikissaksoknari/akaera/senda-til-fmst/[id]/[defendantId].ts b/apps/judicial-system/web/pages/rikissaksoknari/akaera/senda-til-fmst/[id]/[defendantId].ts new file mode 100644 index 000000000000..a5a2b497f1be --- /dev/null +++ b/apps/judicial-system/web/pages/rikissaksoknari/akaera/senda-til-fmst/[id]/[defendantId].ts @@ -0,0 +1,3 @@ +import SendToPrisonAdmin from '@island.is/judicial-system-web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin' + +export default SendToPrisonAdmin diff --git a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts index eae8e0f09ca1..a68972df8229 100644 --- a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts +++ b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.strings.ts @@ -2,8 +2,8 @@ import { defineMessages } from 'react-intl' export const strings = defineMessages({ keyDates: { - id: 'judicial.system.core:blue_box_with_date.key_dates', - defaultMessage: 'Lykildagsetningar', + id: 'judicial.system.core:blue_box_with_date.key_dates_v1', + defaultMessage: 'Birting dóms', description: 'Notaður sem titill í lykildagsetningarsvæði dómfellda.', }, defendantVerdictViewDateLabel: { @@ -52,4 +52,22 @@ export const strings = defineMessages({ description: 'Notaður sem text í takka til að skrá hvenær dómur var áfrýjaður af dómfellda.', }, + sendToPrisonAdmin: { + id: 'judicial.system.core:blue_box_with_date.send_to_fmst', + defaultMessage: 'Senda til fullnustu', + description: + 'Notaður sem texti í valmynd fyrir aðgerðina að senda mál til fullnustu', + }, + revokeSendToPrisonAdmin: { + id: 'judicial.system.core:blue_box_with_date.revoke_send_to_fmst', + defaultMessage: 'Afturkalla úr fullnustu', + description: + 'Notaður sem texti í valmynd fyrir aðgerðina að senda mál til fullnustu', + }, + sendToPrisonAdminDate: { + id: 'judicial.system.core:blue_box_with_date.send_to_fmst_date', + defaultMessage: 'Sent til fullnustu {date}', + description: + 'Notaður sem texti í valmynd fyrir aðgerðina að senda mál til fullnustu', + }, }) diff --git a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx index f5e499d305b7..6a404390e8c9 100644 --- a/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx +++ b/apps/judicial-system/web/src/components/BlueBoxWithIcon/BlueBoxWithDate.tsx @@ -1,7 +1,8 @@ -import { FC, useContext, useEffect, useState } from 'react' +import { FC, useContext, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import addDays from 'date-fns/addDays' import { AnimatePresence, motion } from 'framer-motion' +import { useRouter } from 'next/router' import { Box, @@ -11,13 +12,14 @@ import { Text, toast, } from '@island.is/island-ui/core' +import { PUBLIC_PROSECUTOR_STAFF_INDICTMENT_SEND_TO_PRISON_ADMIN_ROUTE } from '@island.is/judicial-system/consts' import { formatDate } from '@island.is/judicial-system/formatters' import { VERDICT_APPEAL_WINDOW_DAYS } from '@island.is/judicial-system/types' import { errors } from '@island.is/judicial-system-web/messages' import { - CaseIndictmentRulingDecision, Defendant, + IndictmentCaseReviewDecision, ServiceRequirement, } from '../../graphql/schema' import { formatDateForServer, useDefendants } from '../../utils/hooks' @@ -30,22 +32,25 @@ import * as styles from './BlueBoxWithIcon.css' interface Props { defendant: Defendant - indictmentRulingDecision?: CaseIndictmentRulingDecision | null + indictmentReviewDecision?: IndictmentCaseReviewDecision | null icon?: IconMapIcon } -type DateType = 'verdictViewDate' | 'appealDate' - const BlueBoxWithDate: FC = (props) => { - const { defendant, indictmentRulingDecision, icon } = props + const { defendant, indictmentReviewDecision, icon } = props const { formatMessage } = useIntl() - const [verdictViewDate, setVerdictViewDate] = useState() - const [appealDate, setAppealDate] = useState() - const [textItems, setTextItems] = useState([]) + const [dates, setDates] = useState<{ + verdictViewDate?: Date + verdictAppealDate?: Date + }>({ + verdictViewDate: undefined, + verdictAppealDate: undefined, + }) const [triggerAnimation, setTriggerAnimation] = useState(false) const [triggerAnimation2, setTriggerAnimation2] = useState(false) const { setAndSendDefendantToServer } = useDefendants() const { workingCase, setWorkingCase } = useContext(FormContext) + const router = useRouter() const serviceRequired = defendant.serviceRequirement === ServiceRequirement.REQUIRED @@ -53,7 +58,7 @@ const BlueBoxWithDate: FC = (props) => { const handleDateChange = ( date: Date | undefined, valid: boolean, - dateType: DateType, + type: keyof typeof dates, ) => { if (!date) { // Do nothing @@ -65,44 +70,112 @@ const BlueBoxWithDate: FC = (props) => { return } - dateType === 'verdictViewDate' - ? setVerdictViewDate(date) - : setAppealDate(date) + setDates((prev) => ({ ...prev, [type]: date })) } - const handleSetDate = (dateType: DateType) => { - if ( - (dateType === 'verdictViewDate' && !verdictViewDate) || - (dateType === 'appealDate' && !appealDate) - ) { + const handleSetDate = (type: keyof typeof dates) => { + const date = dates[type] + + if (!date) { toast.error(formatMessage(errors.invalidDate)) return } - if (dateType === 'verdictViewDate' && verdictViewDate) { - setAndSendDefendantToServer( - { - caseId: workingCase.id, - defendantId: defendant.id, - verdictViewDate: formatDateForServer(verdictViewDate), - }, - setWorkingCase, - ) - } else if (dateType === 'appealDate' && appealDate) { + const payload = { + caseId: workingCase.id, + defendantId: defendant.id, + [type]: formatDateForServer(date), + } + + setAndSendDefendantToServer(payload, setWorkingCase) + + if (type === 'verdictAppealDate') { setTriggerAnimation2(true) - setAndSendDefendantToServer( - { - caseId: workingCase.id, - defendantId: defendant.id, - verdictAppealDate: formatDateForServer(appealDate), - }, - setWorkingCase, - ) - } else { - toast.error(formatMessage(errors.general)) } } + const handleSendToPrisonAdmin = () => { + router.push( + `${PUBLIC_PROSECUTOR_STAFF_INDICTMENT_SEND_TO_PRISON_ADMIN_ROUTE}/${workingCase.id}/${defendant.id}`, + ) + } + + const handleRevokeSendToPrisonAdmin = () => { + setAndSendDefendantToServer( + { + caseId: workingCase.id, + defendantId: defendant.id, + isSentToPrisonAdmin: false, + }, + setWorkingCase, + ) + } + + const appealExpirationInfo = useMemo(() => { + const deadline = + defendant.verdictAppealDeadline || + (dates.verdictViewDate && + addDays( + dates.verdictViewDate, + VERDICT_APPEAL_WINDOW_DAYS, + ).toISOString()) + + return getAppealExpirationInfo( + deadline, + defendant.isVerdictAppealDeadlineExpired, + ) + }, [ + dates.verdictViewDate, + defendant.isVerdictAppealDeadlineExpired, + defendant.verdictAppealDeadline, + ]) + + const textItems = useMemo(() => { + const texts = [] + + if (serviceRequired) { + texts.push( + formatMessage(strings.defendantVerdictViewedDate, { + date: formatDate(dates.verdictViewDate ?? defendant.verdictViewDate), + }), + ) + } + + texts.push( + formatMessage(appealExpirationInfo.message, { + appealExpirationDate: appealExpirationInfo.date, + }), + ) + + if (defendant.verdictAppealDate) { + texts.push( + formatMessage(strings.defendantAppealDate, { + date: formatDate(defendant.verdictAppealDate), + }), + ) + } + + if (defendant.sentToPrisonAdminDate && defendant.isSentToPrisonAdmin) { + texts.push( + formatMessage(strings.sendToPrisonAdminDate, { + date: formatDate(defendant.sentToPrisonAdminDate), + }), + ) + } + + return texts + }, [ + appealExpirationInfo.date, + appealExpirationInfo.message, + dates.verdictViewDate, + defendant.isSentToPrisonAdmin, + defendant.sentToPrisonAdminDate, + defendant.verdictAppealDate, + defendant.verdictViewDate, + formatMessage, + serviceRequired, + ]) + const datePickerVariants = { dpHidden: { opacity: 0, y: 15, marginTop: '16px' }, dpVisible: { opacity: 1, y: 0 }, @@ -127,162 +200,141 @@ const BlueBoxWithDate: FC = (props) => { }, } - useEffect(() => { - const verdictAppealDeadline = defendant.verdictAppealDeadline - ? defendant.verdictAppealDeadline - : verdictViewDate - ? addDays( - new Date(verdictViewDate), - VERDICT_APPEAL_WINDOW_DAYS, - ).toISOString() - : null - - const appealExpiration = getAppealExpirationInfo( - verdictAppealDeadline, - defendant.isVerdictAppealDeadlineExpired, - ) - - setTextItems([ - ...(serviceRequired - ? [ - formatMessage(strings.defendantVerdictViewedDate, { - date: verdictViewDate - ? formatDate(verdictViewDate) - : formatDate(defendant.verdictViewDate), - }), - ] - : []), - formatMessage(appealExpiration.message, { - appealExpirationDate: appealExpiration.date, - }), - ...(defendant.verdictAppealDate - ? [ - formatMessage(strings.defendantAppealDate, { - date: formatDate(defendant.verdictAppealDate), - }), - ] - : []), - ]) - }, [ - defendant.isVerdictAppealDeadlineExpired, - defendant.verdictAppealDate, - defendant.verdictAppealDeadline, - defendant.verdictViewDate, - formatMessage, - indictmentRulingDecision, - serviceRequired, - verdictViewDate, - ]) - return ( - - - - {icon && ( - - )} - - {defendant.name} + <> + + + + {icon && ( + + )} + + {defendant.name} + - - - {(!serviceRequired || defendant.verdictViewDate) && - textItems.map((text, index) => ( + + {(!serviceRequired || defendant.verdictViewDate) && + textItems.map((text, index) => ( + setTriggerAnimation(true)} + > + {`• ${text}`} + + ))} + + + {defendant.verdictAppealDate || + defendant.isVerdictAppealDeadlineExpired ? null : !serviceRequired || + defendant.verdictViewDate ? ( + + + + handleDateChange(date, valid, 'verdictAppealDate') + } + maxDate={new Date()} + blueBox={false} + dateOnly + /> + + + + ) : ( setTriggerAnimation(true)} + key="defendantVerdictViewDate" + variants={datePickerVariants} + initial={false} + animate="dpVisible" + exit="dpExit" + transition={{ duration: 0.2, ease: 'easeInOut', delay: 0.4 }} > - {`• ${text}`} + + + handleDateChange(date, valid, 'verdictViewDate') + } + blueBox={false} + maxDate={new Date()} + dateOnly + /> + + - ))} - - - {defendant.verdictAppealDate || - defendant.isVerdictAppealDeadlineExpired ? null : !serviceRequired || - defendant.verdictViewDate ? ( - + + + {defendant.isSentToPrisonAdmin ? ( + - - + {formatMessage(strings.revokeSendToPrisonAdmin)} + ) : ( - - - - handleDateChange(date, valid, 'verdictViewDate') - } - blueBox={false} - maxDate={new Date()} - dateOnly - /> - - - + {formatMessage(strings.sendToPrisonAdmin)} + )} - - + + ) } diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index d80ada64e561..f2a5e28a0aa0 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -33,6 +33,8 @@ query Case($input: CaseQueryInput!) { verdictAppealDate isVerdictAppealDeadlineExpired subpoenaType + isSentToPrisonAdmin + sentToPrisonAdminDate subpoenas { id created diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index 1e09fc783d50..df8b33ad5c86 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -45,6 +45,8 @@ query LimitedAccessCase($input: CaseQueryInput!) { verdictAppealDeadline isVerdictAppealDeadlineExpired subpoenaType + isSentToPrisonAdmin + sentToPrisonAdminDate subpoenas { id created diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts index 0ef67882b9ac..3bdd0a941950 100644 --- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts @@ -48,4 +48,9 @@ export const strings = defineMessages({ defaultMessage: 'Hefur ekki verið skráður', description: 'Notaður sem texti þegar enginn verjandi er skráður.', }, + sendToPrisonAdminDate: { + id: 'judicial.system.core:info_card.send_to_prison_admin_date', + defaultMessage: 'Sent til fullnustu {date}', + description: 'Notaður sem texti fyrir hvenær mál var sent til fullnustu', + }, }) diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx index eadbf98ea3c6..45a74dd89427 100644 --- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx @@ -26,6 +26,7 @@ interface DefendantInfoProps { defendant: Defendant displayAppealExpirationInfo?: boolean displayVerdictViewDate?: boolean + displaySentToPrisonAdminDate?: boolean defender?: Defender } @@ -69,6 +70,7 @@ export const DefendantInfo: FC = (props) => { defendant, displayAppealExpirationInfo, displayVerdictViewDate, + displaySentToPrisonAdminDate = true, defender, } = props const { formatMessage } = useIntl() @@ -139,6 +141,13 @@ export const DefendantInfo: FC = (props) => { {getVerdictViewDateText(formatMessage, defendant.verdictViewDate)} )} + {displaySentToPrisonAdminDate && defendant.sentToPrisonAdminDate && ( + + {formatMessage(strings.sendToPrisonAdminDate, { + date: formatDate(defendant.sentToPrisonAdminDate), + })} + + )} ) } diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx index a2e2a8d44204..88391ea722d7 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx @@ -8,6 +8,7 @@ import useInfoCardItems from './useInfoCardItems' export interface Props { displayAppealExpirationInfo?: boolean displayVerdictViewDate?: boolean + displaySentToPrisonAdminDate?: boolean } const InfoCardClosedIndictment: FC = (props) => { @@ -29,7 +30,11 @@ const InfoCardClosedIndictment: FC = (props) => { civilClaimants, } = useInfoCardItems() - const { displayAppealExpirationInfo, displayVerdictViewDate } = props + const { + displayAppealExpirationInfo, + displayVerdictViewDate, + displaySentToPrisonAdminDate, + } = props const reviewedDate = workingCase.eventLogs?.find( (log) => log.eventType === EventType.INDICTMENT_REVIEWED, @@ -45,6 +50,7 @@ const InfoCardClosedIndictment: FC = (props) => { workingCase.type, displayAppealExpirationInfo, displayVerdictViewDate, + displaySentToPrisonAdminDate, ), ], }, diff --git a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx index 4490433a825c..1a0c7ccf70c3 100644 --- a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx @@ -36,6 +36,7 @@ const useInfoCardItems = () => { caseType?: CaseType | null, displayAppealExpirationInfo?: boolean, displayVerdictViewDate?: boolean, + displaySentToPrisonAdminDate?: boolean, ): Item => { const defendants = workingCase.defendants const isMultipleDefendants = defendants && defendants.length > 1 @@ -78,6 +79,7 @@ const useInfoCardItems = () => { }} displayAppealExpirationInfo={displayAppealExpirationInfo} displayVerdictViewDate={displayVerdictViewDate} + displaySentToPrisonAdminDate={displaySentToPrisonAdminDate} /> )) diff --git a/apps/judicial-system/web/src/components/Modal/Modal.tsx b/apps/judicial-system/web/src/components/Modal/Modal.tsx index f5778bbf4c5a..cce5de20aa11 100644 --- a/apps/judicial-system/web/src/components/Modal/Modal.tsx +++ b/apps/judicial-system/web/src/components/Modal/Modal.tsx @@ -22,6 +22,7 @@ interface ModalProps { errorMessage?: string children?: ReactNode invertButtonColors?: boolean + loading?: boolean } const Modal: FC> = ({ @@ -39,6 +40,7 @@ const Modal: FC> = ({ errorMessage, children, invertButtonColors, + loading, }: ModalProps) => { const modalVariants = { open: { @@ -73,7 +75,11 @@ const Modal: FC> = ({ > {onClose && ( - @@ -100,6 +106,7 @@ const Modal: FC> = ({ variant={invertButtonColors ? undefined : 'ghost'} onClick={onSecondaryButtonClick} loading={isSecondaryButtonLoading} + disabled={loading} > {secondaryButtonText} @@ -146,6 +153,7 @@ const ModalPortal = ({ errorMessage, children, invertButtonColors, + loading, }: ModalProps) => { const modalRoot = document.getElementById('modal') ?? document.createElement('div') @@ -166,6 +174,7 @@ const ModalPortal = ({ errorMessage={errorMessage} children={children} invertButtonColors={invertButtonColors} + loading={loading} />, modalRoot, ) diff --git a/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx b/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx index a223be1ba987..b2c97fed3ee6 100644 --- a/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx +++ b/apps/judicial-system/web/src/components/SectionHeading/SectionHeading.tsx @@ -8,7 +8,7 @@ interface Props { title: string required?: boolean tooltip?: ReactNode - description?: ReactNode + description?: ReactNode | string marginBottom?: ResponsiveProp heading?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' } @@ -31,7 +31,11 @@ const SectionHeading: FC = ({ {description && ( - {description} + {typeof description === 'string' ? ( + {description} + ) : ( + description + )} )} 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 888fcdaf4d1c..6ac0b378751e 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 @@ -104,13 +104,13 @@ export const Overview = () => { ))} - + {/* NOTE: Temporarily hidden while list of laws broken is not complete in diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.strings.ts new file mode 100644 index 000000000000..6a513f738833 --- /dev/null +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.strings.ts @@ -0,0 +1,44 @@ +import { defineMessages } from 'react-intl' + +export const strings = defineMessages({ + title: { + id: 'judicial.system.core:send_to_prison_admin.title', + defaultMessage: 'Senda til fullnustu', + description: 'Notaður sem titill á senda til fullnustu.', + }, + fileUploadTitle: { + id: 'judicial.system.core:send_to_prison_admin.file_upload_title', + defaultMessage: 'Gögn sem þurfa að fylgja með til fullnustu', + description: 'Notaður sem titill fyrir gagna upphlaðningu.', + }, + fileUploadDescription: { + id: 'judicial.system.core:send_to_prison_admin.file_upload_description', + defaultMessage: + 'Ef dómur er sendur til fullnustu áður en áfrýjunarfrestur ákærða er liðinn þarf að fylgja með bréf frá verjanda.', + description: 'Notaður sem texti fyrir gagna upphlaðningu.', + }, + nextButtonText: { + id: 'judicial.system.core:send_to_prison_admin.next_button_text', + defaultMessage: 'Senda til fullnustu', + description: 'Notaður sem texti í áfram takka.', + }, + modalTitle: { + id: 'judicial.system.core:send_to_prison_admin.modal_title', + defaultMessage: 'Senda til fullnustu', + description: + 'Notaður sem titill á tilkynningarglugga um að senda til fullnustu.', + }, + modalText: { + id: 'judicial.system.core:send_to_prison_admin.modal_text', + defaultMessage: + 'Mál {courtCaseNumber} verður sent til Fangelsismálastofnunar til fullnustu.\nÁkærði: {defendant}.', + description: + 'Notaður sem texti á tilkynningarglugga um að senda til fullnustu', + }, + modalNextButtonText: { + id: 'judicial.system.core:send_to_prison_admin.modal_next_button_text', + defaultMessage: 'Senda núna', + description: + 'Notaður sem texti í takka á tilkynningarglugga um að senda til fullnustu', + }, +}) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.tsx new file mode 100644 index 000000000000..84ba3f0e18ad --- /dev/null +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/SendToPrisonAdmin/SendToPrisonAdmin.tsx @@ -0,0 +1,169 @@ +import { FC, useCallback, useContext, useState } from 'react' +import { useIntl } from 'react-intl' +import { useParams, useRouter } from 'next/navigation' + +import { Box, UploadFile } from '@island.is/island-ui/core' +import { PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE } from '@island.is/judicial-system/consts' +import { core } from '@island.is/judicial-system-web/messages' +import { + CourtCaseInfo, + FormContentContainer, + FormContext, + FormFooter, + Modal, + PageHeader, + PageLayout, + PageTitle, + SectionHeading, +} from '@island.is/judicial-system-web/src/components' +import { CaseFileCategory } from '@island.is/judicial-system-web/src/graphql/schema' +import { + useDefendants, + useS3Upload, + useUploadFiles, +} from '@island.is/judicial-system-web/src/utils/hooks' + +import { strings } from './SendToPrisonAdmin.strings' + +enum AvailableModal { + SUCCESS = 'SUCCESS', +} + +const SendToPrisonAdmin: FC = () => { + const { workingCase, isLoadingWorkingCase, caseNotFound } = + useContext(FormContext) + const { formatMessage } = useIntl() + const [modalVisible, setModalVisible] = useState() + const [uploadFileError, setUploadFileError] = useState() + const router = useRouter() + const { defendantId } = useParams<{ caseId: string; defendantId: string }>() + const { handleUpload, handleRemove } = useS3Upload(workingCase.id) + const { updateDefendant, isUpdatingDefendant } = useDefendants() + const { uploadFiles, removeUploadFile, addUploadFiles, updateUploadFile } = + useUploadFiles() + + const defendant = workingCase.defendants?.find( + (defendant) => defendant.id === defendantId, + ) + + const handleNextButtonClick = useCallback(async () => { + setModalVisible(AvailableModal.SUCCESS) + }, []) + + const handleSecondaryButtonClick = () => { + setModalVisible(undefined) + } + + const handlePrimaryButtonClick = async () => { + if (!defendant) { + return + } + + await updateDefendant({ + defendantId: defendant.id, + caseId: workingCase.id, + isSentToPrisonAdmin: true, + }) + + // TODO: UNCOMMENT WHEN THIS FEATURE IS READY + // const uploadResult = await handleUpload( + // uploadFiles.filter((file) => file.percent === 0), + // updateUploadFile, + // ) + + // if (uploadResult !== 'ALL_SUCCEEDED') { + // setUploadFileError(formatMessage(errors.uploadFailed)) + // return + // } + + router.push( + `${PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE}/${workingCase.id}`, + ) + } + + const handleFileUpload = useCallback( + (files: File[]) => { + addUploadFiles(files, { + category: CaseFileCategory.SENT_TO_PRISON_ADMIN_FILE, + status: 'done', + }) + }, + [addUploadFiles], + ) + + const handleRemoveFile = useCallback( + (file: UploadFile) => { + if (file.key) { + handleRemove(file, removeUploadFile) + } else { + removeUploadFile(file) + } + }, + [handleRemove, removeUploadFile], + ) + + return ( + + + + {formatMessage(strings.title)} + + + + {/* NOTE: This is temporarily disabled while we work on this + upload feature. + + file.category === CaseFileCategory.SENT_TO_PRISON_ADMIN_FILE, + )} + accept="application/pdf" + header={formatMessage(core.uploadBoxTitle)} + description={formatMessage(core.uploadBoxDescription, { + fileEndings: '.pdf', + })} + buttonLabel={formatMessage(core.uploadBoxButtonLabel)} + onChange={handleFileUpload} + onRemove={handleRemoveFile} + /> */} + + + + + + {modalVisible === AvailableModal.SUCCESS && defendant && ( + + )} + + ) +} + +export default SendToPrisonAdmin diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts index 86650607956f..85cb3bb24b15 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.strings.ts @@ -48,4 +48,10 @@ export const strings = defineMessages({ description: 'Notað sem texti á taggi fyrir "Dómur birtur" í yfirlesin mál málalista', }, + tagVerdictViewSentToPrisonAdmin: { + id: 'judicial.system.core:public_prosecutor.tables.cases_reviewed.tag_verdict_sent_to_prison_admin', + defaultMessage: 'Til fullnustu', + description: + 'Notað sem texti á taggi fyrir "Til fullnustu" í yfirlesin mál málalista', + }, }) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx index 6c483c7aedf2..e6a4b98ab6c2 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Tables/CasesReviewed.tsx @@ -1,5 +1,5 @@ import { FC } from 'react' -import { useIntl } from 'react-intl' +import { MessageDescriptor, useIntl } from 'react-intl' import { AnimatePresence } from 'framer-motion' import { Tag, Text } from '@island.is/island-ui/core' @@ -41,16 +41,28 @@ const CasesReviewed: FC = ({ loading, cases }) => { } const getVerdictViewTag = (row: CaseListEntry) => { - const variant = !row.indictmentVerdictViewedByAll - ? 'red' - : row.indictmentVerdictAppealDeadlineExpired - ? 'mint' - : 'blue' - const message = !row.indictmentVerdictViewedByAll - ? strings.tagVerdictUnviewed - : row.indictmentVerdictAppealDeadlineExpired - ? strings.tagVerdictViewComplete - : strings.tagVerdictViewOnDeadline + let variant: 'red' | 'mint' | 'blue' + let message: MessageDescriptor + + const someDefendantIsSentToPrisonAdmin = Boolean( + row.defendants?.length && + row.defendants.some((defendant) => defendant.isSentToPrisonAdmin), + ) + + if (someDefendantIsSentToPrisonAdmin) { + variant = 'red' + message = strings.tagVerdictViewSentToPrisonAdmin + } else if (!row.indictmentVerdictViewedByAll) { + variant = 'red' + message = strings.tagVerdictUnviewed + } else if (row.indictmentVerdictAppealDeadlineExpired) { + variant = 'mint' + message = strings.tagVerdictViewComplete + } else { + variant = 'blue' + message = strings.tagVerdictViewOnDeadline + } + return ( {formatMessage(message)} 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 62c0d11b2d01..d35f7fb9af54 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql +++ b/apps/judicial-system/web/src/routes/Shared/Cases/cases.graphql @@ -27,6 +27,7 @@ query Cases { noNationalId defenderChoice verdictViewDate + isSentToPrisonAdmin } courtDate isValidToDateInThePast diff --git a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts index 66987e7b513b..49da94987d47 100644 --- a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts @@ -20,7 +20,8 @@ const useDefendants = () => { const [createDefendantMutation, { loading: isCreatingDefendant }] = useCreateDefendantMutation() const [deleteDefendantMutation] = useDeleteDefendantMutation() - const [updateDefendantMutation] = useUpdateDefendantMutation() + const [updateDefendantMutation, { loading: isUpdatingDefendant }] = + useUpdateDefendantMutation() const createDefendant = useCallback( async (defendant: CreateDefendantInput) => { @@ -118,6 +119,7 @@ const useDefendants = () => { createDefendant, deleteDefendant, updateDefendant, + isUpdatingDefendant, updateDefendantState, setAndSendDefendantToServer, } diff --git a/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts b/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts index d8a89c385f64..ade728d7ccae 100644 --- a/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useEventLog/index.ts @@ -29,6 +29,8 @@ const useEventLog = () => { [createEventLogMutation, formatMessage], ) - return { createEventLog } + return { + createEventLog, + } } export default useEventLog diff --git a/libs/application/api/files/src/lib/upload.processor.ts b/libs/application/api/files/src/lib/upload.processor.ts index 03125a7fa48b..4b03fac09c07 100644 --- a/libs/application/api/files/src/lib/upload.processor.ts +++ b/libs/application/api/files/src/lib/upload.processor.ts @@ -6,6 +6,7 @@ import { Inject } from '@nestjs/common' import AmazonS3URI from 'amazon-s3-uri' import { ApplicationFilesConfig } from './files.config' import { ConfigType } from '@nestjs/config' +import { LOGGER_PROVIDER, type Logger } from '@island.is/logging' interface JobData { applicationId: string @@ -25,54 +26,69 @@ export class UploadProcessor { private readonly fileStorageService: FileStorageService, @Inject(ApplicationFilesConfig.KEY) private config: ConfigType, + @Inject(LOGGER_PROVIDER) protected readonly logger: Logger, ) {} @Process('upload') async handleUpload(job: Job): Promise { - const { attachmentUrl, applicationId } = job.data - const destinationBucket = this.config.attachmentBucket + try { + const { attachmentUrl, applicationId } = job.data + const destinationBucket = this.config.attachmentBucket - if (!destinationBucket) { - throw new Error('Application attachment bucket not configured.') - } + if (!destinationBucket) { + throw new Error('Application attachment bucket not configured.') + } - const { key: sourceKey } = AmazonS3URI(attachmentUrl) - const destinationKey = `${applicationId}/${sourceKey}` - const resultUrl = await this.fileStorageService.copyObjectFromUploadBucket( - sourceKey, - destinationBucket, - destinationKey, - ) + const { key: sourceKey } = AmazonS3URI(attachmentUrl) + const destinationKey = `${applicationId}/${sourceKey}` + const resultUrl = + await this.fileStorageService.copyObjectFromUploadBucket( + sourceKey, + destinationBucket, + destinationKey, + ) - return { - attachmentKey: sourceKey, - resultUrl, + return { + attachmentKey: sourceKey, + resultUrl, + } + } catch (error) { + this.logger.error('An error occurred while processing upload job', error) + throw error } } @OnQueueCompleted() async onCompleted(job: Job, result: JobResult) { - const { applicationId, nationalId }: JobData = job.data - const existingApplication = await this.applicationService.findOneById( - applicationId, - nationalId, - ) + try { + const { applicationId, nationalId }: JobData = job.data + const existingApplication = await this.applicationService.findOneById( + applicationId, + nationalId, + ) - if ( - existingApplication && - !Object.prototype.hasOwnProperty.call( - existingApplication.attachments, + // If the application exists + // And the attachments object doesnt have a property with the given key + // Dont update it with the new storage s3 url (because it doesnt exist) + if ( + existingApplication && + !Object.prototype.hasOwnProperty.call( + existingApplication.attachments, + result.attachmentKey, + ) + ) { + return + } + + return await this.applicationService.updateAttachment( + applicationId, + nationalId, result.attachmentKey, + result.resultUrl, ) - ) { - return + } catch (error) { + this.logger.error('An error occurred while completing upload job', error) + throw error } - - return await this.applicationService.updateAttachment( - applicationId, - nationalId, - result.attachmentKey, - result.resultUrl, - ) } } diff --git a/libs/application/types/src/lib/Form.ts b/libs/application/types/src/lib/Form.ts index 66665519be10..e3656d70d405 100644 --- a/libs/application/types/src/lib/Form.ts +++ b/libs/application/types/src/lib/Form.ts @@ -185,6 +185,7 @@ export interface FieldBaseProps { application: Application showFieldName?: boolean goToScreen?: (id: string) => void + answerQuestions?: (answers: FormValue) => void refetch?: () => void setBeforeSubmitCallback?: SetBeforeSubmitCallback setFieldLoadingState?: SetFieldLoadingState diff --git a/libs/application/ui-components/src/utilities/FileUploadController/index.tsx b/libs/application/ui-components/src/utilities/FileUploadController/index.tsx index 25d69e923d16..6270b9d8ad2d 100644 --- a/libs/application/ui-components/src/utilities/FileUploadController/index.tsx +++ b/libs/application/ui-components/src/utilities/FileUploadController/index.tsx @@ -65,6 +65,7 @@ interface FileUploadControllerProps { readonly id: string error?: string application: Application + onRemove?: (f: UploadFile) => void readonly header?: string readonly description?: string readonly buttonLabel?: string @@ -89,6 +90,7 @@ export const FileUploadController = ({ maxSizeErrorText, totalMaxSize = DEFAULT_TOTAL_MAX_SIZE, forImageUpload, + onRemove, }: FileUploadControllerProps) => { const { formatMessage } = useLocale() const { clearErrors, setValue } = useFormContext() @@ -232,6 +234,8 @@ export const FileUploadController = ({ } } + onRemove?.(fileToRemove) + // We remove it from the list if: the delete attachment above succeeded, // or if the user clicked x for a file that failed to upload and is in // an error state. diff --git a/libs/application/ui-fields/src/lib/FileUploadFormField/FileUploadFormField.tsx b/libs/application/ui-fields/src/lib/FileUploadFormField/FileUploadFormField.tsx index bc83035945d0..e97e87f0573b 100644 --- a/libs/application/ui-fields/src/lib/FileUploadFormField/FileUploadFormField.tsx +++ b/libs/application/ui-fields/src/lib/FileUploadFormField/FileUploadFormField.tsx @@ -1,15 +1,22 @@ import { formatText } from '@island.is/application/core' import { FieldBaseProps, FileUploadField } from '@island.is/application/types' -import { Box } from '@island.is/island-ui/core' +import { Box, UploadFile } from '@island.is/island-ui/core' import { FieldDescription } from '@island.is/shared/form-fields' import { useLocale } from '@island.is/localization' import { FileUploadController } from '@island.is/application/ui-components' +import { useFormContext } from 'react-hook-form' +import set from 'lodash/set' interface Props extends FieldBaseProps { field: FileUploadField } -export const FileUploadFormField = ({ application, field, error }: Props) => { +export const FileUploadFormField = ({ + application, + answerQuestions, + field, + error, +}: Props) => { const { id, introduction, @@ -22,8 +29,21 @@ export const FileUploadFormField = ({ application, field, error }: Props) => { maxSizeErrorText, forImageUpload, } = field - const { formatMessage } = useLocale() + const { watch } = useFormContext() + const currentValue = watch(id) + + const onFileRemoveWhenInAnswers = (fileToRemove: UploadFile) => { + const updatedAnswers = set( + application.answers, + id, + currentValue.filter((x: UploadFile) => x.key !== fileToRemove.key), + ) + answerQuestions?.({ + ...application.answers, + ...updatedAnswers, + }) + } return (
@@ -57,6 +77,7 @@ export const FileUploadFormField = ({ application, field, error }: Props) => { formatText(maxSizeErrorText, application, formatMessage) } forImageUpload={forImageUpload} + onRemove={onFileRemoveWhenInAnswers} />
diff --git a/libs/application/ui-shell/src/components/FormField.tsx b/libs/application/ui-shell/src/components/FormField.tsx index f0c4ff71c1c7..81b1fe216d6d 100644 --- a/libs/application/ui-shell/src/components/FormField.tsx +++ b/libs/application/ui-shell/src/components/FormField.tsx @@ -8,6 +8,7 @@ import { SetFieldLoadingState, SetBeforeSubmitCallback, SetSubmitButtonDisabled, + FormValue, } from '@island.is/application/types' import { useFields } from '../context/FieldContext' @@ -19,6 +20,7 @@ const FormField: FC< setBeforeSubmitCallback?: SetBeforeSubmitCallback setFieldLoadingState?: SetFieldLoadingState setSubmitButtonDisabled?: SetSubmitButtonDisabled + answerQuestions?: (answers: FormValue) => void autoFocus?: boolean field: FieldDef showFieldName?: boolean @@ -31,6 +33,7 @@ const FormField: FC< setBeforeSubmitCallback, setFieldLoadingState, setSubmitButtonDisabled, + answerQuestions, autoFocus, errors, field, @@ -51,6 +54,7 @@ const FormField: FC< setBeforeSubmitCallback, setFieldLoadingState, setSubmitButtonDisabled, + answerQuestions, autoFocus, error, errors, diff --git a/libs/application/ui-shell/src/components/FormMultiField.tsx b/libs/application/ui-shell/src/components/FormMultiField.tsx index f4e56df2fa7e..976d9da1676c 100644 --- a/libs/application/ui-shell/src/components/FormMultiField.tsx +++ b/libs/application/ui-shell/src/components/FormMultiField.tsx @@ -106,6 +106,7 @@ const FormMultiField: FC< setBeforeSubmitCallback={setBeforeSubmitCallback} setFieldLoadingState={setFieldLoadingState} setSubmitButtonDisabled={setSubmitButtonDisabled} + answerQuestions={answerQuestions} /> diff --git a/libs/application/ui-shell/src/components/Screen.tsx b/libs/application/ui-shell/src/components/Screen.tsx index 195f315f7b28..52bcbcce6625 100644 --- a/libs/application/ui-shell/src/components/Screen.tsx +++ b/libs/application/ui-shell/src/components/Screen.tsx @@ -399,6 +399,7 @@ const Screen: FC> = ({ goToScreen={goToScreen} refetch={refetch} setSubmitButtonDisabled={setSubmitButtonDisabled} + answerQuestions={answerQuestions} /> )} diff --git a/libs/judicial-system/consts/src/lib/consts.ts b/libs/judicial-system/consts/src/lib/consts.ts index 04ed47326b0c..90c4c60196a0 100644 --- a/libs/judicial-system/consts/src/lib/consts.ts +++ b/libs/judicial-system/consts/src/lib/consts.ts @@ -100,6 +100,8 @@ export const DEFENDER_STATEMENT_ROUTE = '/verjandi/greinargerd' //#region Public prosecutor user routes export const PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE = '/rikissaksoknari/akaera/yfirlit' +export const PUBLIC_PROSECUTOR_STAFF_INDICTMENT_SEND_TO_PRISON_ADMIN_ROUTE = + '/rikissaksoknari/akaera/senda-til-fmst' //#endregion Public prosecutor user routes //#region Prison user routes diff --git a/libs/judicial-system/types/src/index.ts b/libs/judicial-system/types/src/index.ts index 4357ea6ad607..2a7cf9901626 100644 --- a/libs/judicial-system/types/src/index.ts +++ b/libs/judicial-system/types/src/index.ts @@ -22,7 +22,12 @@ export { notificationTypes, } from './lib/notification' export type { Institution } from './lib/institution' -export { EventType, eventTypes } from './lib/eventLog' +export { + EventType, + eventTypes, + DefendantEventType, + defendantEventTypes, +} from './lib/eventLog' export { DateType, dateTypes } from './lib/dateLog' export { StringType, stringTypes } from './lib/caseString' diff --git a/libs/judicial-system/types/src/lib/eventLog.ts b/libs/judicial-system/types/src/lib/eventLog.ts index e39c40a5feae..fca7b91f81ea 100644 --- a/libs/judicial-system/types/src/lib/eventLog.ts +++ b/libs/judicial-system/types/src/lib/eventLog.ts @@ -12,3 +12,9 @@ export enum EventType { } export const eventTypes = Object.values(EventType) + +export enum DefendantEventType { + SENT_TO_PRISON_ADMIN = 'SENT_TO_PRISON_ADMIN', +} + +export const defendantEventTypes = Object.values(DefendantEventType) diff --git a/libs/judicial-system/types/src/lib/file.ts b/libs/judicial-system/types/src/lib/file.ts index 9831c81ce0a3..17b320bdfb52 100644 --- a/libs/judicial-system/types/src/lib/file.ts +++ b/libs/judicial-system/types/src/lib/file.ts @@ -28,4 +28,5 @@ export enum CaseFileCategory { APPEAL_COURT_RECORD = 'APPEAL_COURT_RECORD', APPEAL_RULING = 'APPEAL_RULING', CIVIL_CLAIM = 'CIVIL_CLAIM', + SENT_TO_PRISON_ADMIN_FILE = 'SENT_TO_PRISON_ADMIN_FILE', } diff --git a/package.json b/package.json index bb6b0c402daa..e78c508c8747 100644 --- a/package.json +++ b/package.json @@ -309,7 +309,7 @@ "winston": "3.7.2", "winston-cloudwatch": "3.1.1", "winston-transport": "4.6.0", - "xlsx": "0.17.0", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xml2js": "0.4.23", "xstate": "4.20.0", "yargs": "17.7.2", diff --git a/yarn.lock b/yarn.lock index ba4c34531891..dc4f63250639 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24255,18 +24255,6 @@ __metadata: languageName: node linkType: hard -"adler-32@npm:~1.2.0": - version: 1.2.0 - resolution: "adler-32@npm:1.2.0" - dependencies: - exit-on-epipe: ~1.0.1 - printj: ~1.1.0 - bin: - adler32: ./bin/adler32.njs - checksum: aa611316fa224349b3eb1b7fcccc05195c38e425ff2b678fde67c0440b27074c42ad97b03efb7478f045b094f0dcb51a8711a2d5571d8940e1c5c52593af7bf6 - languageName: node - linkType: hard - "agent-base@npm:5": version: 5.1.1 resolution: "agent-base@npm:5.1.1" @@ -27506,17 +27494,6 @@ __metadata: languageName: node linkType: hard -"cfb@npm:^1.1.4": - version: 1.2.0 - resolution: "cfb@npm:1.2.0" - dependencies: - adler-32: ~1.2.0 - crc-32: ~1.2.0 - printj: ~1.1.2 - checksum: 3c1bb1a7459bdff5a76c4fd39b1845c515634be1ca23a32c5eecd42dfd4f9e96bd162920a9664a9d87bc7082a00fe65d1a755404041def37af5699e7ec776f72 - languageName: node - linkType: hard - "chalk@npm:1.1.1": version: 1.1.1 resolution: "chalk@npm:1.1.1" @@ -28288,18 +28265,6 @@ __metadata: languageName: node linkType: hard -"codepage@npm:~1.14.0": - version: 1.14.0 - resolution: "codepage@npm:1.14.0" - dependencies: - commander: ~2.14.1 - exit-on-epipe: ~1.0.1 - bin: - codepage: ./bin/codepage.njs - checksum: ea06cbb7b646029d4e252b143b09048f1dee466d80be9e68cc4da05981f982a02f1199db0ea27d5e05a026bfa77b91e3ae8e0d3ed9665bd77ba54911bf2b82b7 - languageName: node - linkType: hard - "collect-v8-coverage@npm:^1.0.0": version: 1.0.1 resolution: "collect-v8-coverage@npm:1.0.1" @@ -28491,20 +28456,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:~2.14.1": - version: 2.14.1 - resolution: "commander@npm:2.14.1" - checksum: 26bd49febeac8efabb7488fb5a4a2480b04bc4c4eef3c50da93eead72959f7a5232d003deda5b9761937205721274e80108f6d1d2b45ae7a8387cfb92031084e - languageName: node - linkType: hard - -"commander@npm:~2.17.1": - version: 2.17.1 - resolution: "commander@npm:2.17.1" - checksum: 22e7ed5b422079a13a496e5eb8f73f36c15b5809d46f738e168e20f9ad485c12951bdc2cb366a36eb5f4927dae4f17b355b8adb96a5b9093f5fa4c439e8b9419 - languageName: node - linkType: hard - "comment-json@npm:4.2.3": version: 4.2.3 resolution: "comment-json@npm:4.2.3" @@ -29394,7 +29345,7 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.0, crc-32@npm:~1.2.0": +"crc-32@npm:^1.2.0": version: 1.2.0 resolution: "crc-32@npm:1.2.0" dependencies: @@ -34262,13 +34213,6 @@ __metadata: languageName: node linkType: hard -"fflate@npm:^0.3.8": - version: 0.3.11 - resolution: "fflate@npm:0.3.11" - checksum: 1eca8d3e86fda9f82c8acd16a970dd20bfeb4bd31c7808351ef413bc2673911d2aedc2fed6e8a4b68f5f2598b07f0579fdf6f085f5a8105b49cde8a8f3528def - languageName: node - linkType: hard - "figlet@npm:^1.1.1, figlet@npm:^1.2.0": version: 1.5.2 resolution: "figlet@npm:1.5.2" @@ -34936,13 +34880,6 @@ __metadata: languageName: node linkType: hard -"frac@npm:~1.1.2": - version: 1.1.2 - resolution: "frac@npm:1.1.2" - checksum: fbfbb28003bb84506dd35e7aad8543c5a358bdc95451d0065b6127d40d2c45106f14221575c3e9ce3ea4bf0bbf1225b73c5d655965c9f4ce44332cbe1b34667d - languageName: node - linkType: hard - "fraction.js@npm:^4.2.0": version: 4.2.0 resolution: "fraction.js@npm:4.2.0" @@ -39000,7 +38937,7 @@ __metadata: winston: 3.7.2 winston-cloudwatch: 3.1.1 winston-transport: 4.6.0 - xlsx: 0.17.0 + xlsx: "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" xml2js: 0.4.23 xstate: 4.20.0 yargs: 17.7.2 @@ -47766,7 +47703,7 @@ __metadata: languageName: node linkType: hard -"printj@npm:~1.1.0, printj@npm:~1.1.2": +"printj@npm:~1.1.0": version: 1.1.2 resolution: "printj@npm:1.1.2" bin: @@ -52850,15 +52787,6 @@ __metadata: languageName: node linkType: hard -"ssf@npm:~0.11.2": - version: 0.11.2 - resolution: "ssf@npm:0.11.2" - dependencies: - frac: ~1.1.2 - checksum: 6ecef6ae0a90e67dc4b05bc3cca883e2dffad9773b41124af36ee308884e4a29f98bde66d6c8d2bd1ccf5f860ea4f6c49338bd8d733007fc42ebe02dd5295dcf - languageName: node - linkType: hard - "ssh-remote-port-forward@npm:^1.0.4": version: 1.0.4 resolution: "ssh-remote-port-forward@npm:1.0.4" @@ -57390,13 +57318,6 @@ __metadata: languageName: node linkType: hard -"wmf@npm:~1.0.1": - version: 1.0.2 - resolution: "wmf@npm:1.0.2" - checksum: d336acb2c76fa868ef006fbb06c4e64c7c1ed5ff77d16c48a273cf1f4d0a44e35df209b8fde28d93dd4a924d652a9c4fc8a92ad57885a5e437df0b0900769e3b - languageName: node - linkType: hard - "wonka@npm:^4.0.14": version: 4.0.15 resolution: "wonka@npm:4.0.15" @@ -57418,13 +57339,6 @@ __metadata: languageName: node linkType: hard -"word@npm:~0.3.0": - version: 0.3.0 - resolution: "word@npm:0.3.0" - checksum: f84e7061883380c1bcb0d98bd183b7f2b281688011924af7a96d3ed3ee20aeb12cc59a0451b66e5e57520338a056725ff8e0c07b358c0afecf5488a9557c19fe - languageName: node - linkType: hard - "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" @@ -57558,23 +57472,12 @@ __metadata: languageName: node linkType: hard -"xlsx@npm:0.17.0": - version: 0.17.0 - resolution: "xlsx@npm:0.17.0" - dependencies: - adler-32: ~1.2.0 - cfb: ^1.1.4 - codepage: ~1.14.0 - commander: ~2.17.1 - crc-32: ~1.2.0 - exit-on-epipe: ~1.0.1 - fflate: ^0.3.8 - ssf: ~0.11.2 - wmf: ~1.0.1 - word: ~0.3.0 +"xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz": + version: 0.20.3 + resolution: "xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" bin: - xlsx: bin/xlsx.njs - checksum: 64e44eba53b64b989667b11655b88ef59a695aa07cce2415f9edff64ef6a48d936ed33208d98db3654e31769d79c42fd50e96d983d55e0850cb54a591b28960e + xlsx: ./bin/xlsx.njs + checksum: c08ac699e6d31222404871240aa2c2804cb8bae59f0bea08d3293e99756cf0d3122fd34739189edaae1d9b0b493bbb8eb95d021950ee7b5cba9d55ecd6fcb4e4 languageName: node linkType: hard