From fa0238671e5f5a8b8952ebbdab25abe5847588f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Wed, 29 Jan 2025 09:08:27 +0000 Subject: [PATCH] feat(j-s): Speeding (#17589) * Add option SPEEDING * Add legal arguments * Add input fields * Add recordedSpeed and speedLimit to db * Enable seedLimit update * Add validation * Update indictment description * Clear recorded speed and speed limit when removing speedig * Add error handling * Fix comment * Fix lint * Remove css * Make Icon position abs * Rec speed and speed limit is now number * Remove island-ui change * Improve validation * Use Number.isNaN instead of isNaN --- .../dto/updateIndictmentCount.input.ts | 25 +++- .../models/indictmentCount.model.ts | 8 +- .../20250120150345-update-indictment-count.js | 38 +++++ .../dto/updateIndictmentCount.dto.ts | 14 ++ .../models/indictmentCount.model.ts | 8 ++ .../src/components/FormProvider/case.graphql | 2 + .../Indictment/IndictmentCount.strings.ts | 27 +++- .../Indictment/IndictmentCount.tsx | 130 +++++++++++++++++- .../Indictment/IndictmentCountEnum.strings.ts | 5 + .../Indictment/Substance/Substance.tsx | 2 +- .../utils/hooks/useIndictmentCounts/index.ts | 20 ++- .../updateIndictmentCount.graphql | 2 + .../judicial-system/web/src/utils/validate.ts | 10 +- .../types/src/lib/indictmentCount.ts | 2 + 14 files changed, 280 insertions(+), 13 deletions(-) create mode 100644 apps/judicial-system/backend/migrations/20250120150345-update-indictment-count.js diff --git a/apps/judicial-system/api/src/app/modules/indictment-count/dto/updateIndictmentCount.input.ts b/apps/judicial-system/api/src/app/modules/indictment-count/dto/updateIndictmentCount.input.ts index 7e26e223a6b6..f62993c3319f 100644 --- a/apps/judicial-system/api/src/app/modules/indictment-count/dto/updateIndictmentCount.input.ts +++ b/apps/judicial-system/api/src/app/modules/indictment-count/dto/updateIndictmentCount.input.ts @@ -1,7 +1,14 @@ -import { Allow, IsArray, IsEnum, IsOptional } from 'class-validator' +import { + Allow, + IsArray, + IsEnum, + IsNumber, + IsOptional, + Min, +} from 'class-validator' import { GraphQLJSONObject } from 'graphql-type-json' -import { Field, ID, InputType } from '@nestjs/graphql' +import { Field, ID, InputType, Int } from '@nestjs/graphql' import type { SubstanceMap } from '@island.is/judicial-system/types' import { @@ -63,4 +70,18 @@ export class UpdateIndictmentCountInput { @IsEnum(IndictmentSubtype, { each: true }) @Field(() => [IndictmentSubtype], { nullable: true }) readonly indictmentCountSubtypes?: IndictmentSubtype[] + + @Allow() + @IsOptional() + @IsNumber() + @Min(0) + @Field(() => Int, { nullable: true }) + readonly recordedSpeed?: number + + @Allow() + @IsOptional() + @IsNumber() + @Min(0) + @Field(() => Int, { nullable: true }) + readonly speedLimit?: number } diff --git a/apps/judicial-system/api/src/app/modules/indictment-count/models/indictmentCount.model.ts b/apps/judicial-system/api/src/app/modules/indictment-count/models/indictmentCount.model.ts index f3ef5aa954c6..46e15fe3c2e8 100644 --- a/apps/judicial-system/api/src/app/modules/indictment-count/models/indictmentCount.model.ts +++ b/apps/judicial-system/api/src/app/modules/indictment-count/models/indictmentCount.model.ts @@ -1,6 +1,6 @@ import { GraphQLJSONObject } from 'graphql-type-json' -import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql' +import { Field, ID, Int, ObjectType, registerEnumType } from '@nestjs/graphql' import type { SubstanceMap } from '@island.is/judicial-system/types' import { @@ -48,4 +48,10 @@ export class IndictmentCount { @Field(() => [IndictmentSubtype], { nullable: true }) readonly indictmentCountSubtypes?: IndictmentSubtype[] + + @Field(() => Int, { nullable: true }) + readonly recordedSpeed?: number + + @Field(() => Int, { nullable: true }) + readonly speedLimit?: number } diff --git a/apps/judicial-system/backend/migrations/20250120150345-update-indictment-count.js b/apps/judicial-system/backend/migrations/20250120150345-update-indictment-count.js new file mode 100644 index 000000000000..9fdc1a7c88a1 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20250120150345-update-indictment-count.js @@ -0,0 +1,38 @@ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.addColumn( + 'indictment_count', + 'recorded_speed', + { + type: Sequelize.INTEGER, + allowNull: true, + }, + { transaction: t }, + ), + queryInterface.addColumn( + 'indictment_count', + 'speed_limit', + { + type: Sequelize.INTEGER, + allowNull: true, + }, + { transaction: t }, + ), + ]), + ) + }, + async down(queryInterface) { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.removeColumn('indictment_count', 'recorded_speed', { + transaction: t, + }), + queryInterface.removeColumn('indictment_count', 'speed_limit', { + transaction: t, + }), + ]), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts b/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts index 3b12997e7fed..a5c9d57348ff 100644 --- a/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/indictment-count/dto/updateIndictmentCount.dto.ts @@ -1,10 +1,12 @@ import { IsArray, IsEnum, + IsNumber, IsObject, IsOptional, IsString, MaxLength, + Min, } from 'class-validator' import { ApiPropertyOptional } from '@nestjs/swagger' @@ -59,4 +61,16 @@ export class UpdateIndictmentCountDto { @IsEnum(IndictmentSubtype, { each: true }) @ApiPropertyOptional({ enum: IndictmentSubtype, isArray: true }) readonly indictmentCountSubtypes?: IndictmentSubtype[] + + @IsOptional() + @IsNumber() + @Min(0) + @ApiPropertyOptional({ type: Number }) + readonly recordedSpeed?: number + + @IsOptional() + @IsNumber() + @Min(0) + @ApiPropertyOptional({ type: Number }) + readonly speedLimit?: number } diff --git a/apps/judicial-system/backend/src/app/modules/indictment-count/models/indictmentCount.model.ts b/apps/judicial-system/backend/src/app/modules/indictment-count/models/indictmentCount.model.ts index 03b9d511f2d7..86926e47fcc4 100644 --- a/apps/judicial-system/backend/src/app/modules/indictment-count/models/indictmentCount.model.ts +++ b/apps/judicial-system/backend/src/app/modules/indictment-count/models/indictmentCount.model.ts @@ -80,4 +80,12 @@ export class IndictmentCount extends Model { }) @ApiPropertyOptional({ enum: IndictmentSubtype, isArray: true }) indictmentCountSubtypes?: IndictmentSubtype[] + + @Column({ type: DataType.INTEGER, allowNull: true }) + @ApiPropertyOptional({ type: Number }) + recordedSpeed?: number + + @Column({ type: DataType.INTEGER, allowNull: true }) + @ApiPropertyOptional({ type: Number }) + speedLimit?: number } diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index e607891d482d..b39611982adf 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -219,6 +219,8 @@ query Case($input: CaseQueryInput!) { incidentDescription legalArguments indictmentCountSubtypes + recordedSpeed + speedLimit } requestDriversLicenseSuspension appealState diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.strings.ts b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.strings.ts index dcb0a60fa801..229aaed81626 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.strings.ts +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.strings.ts @@ -95,6 +95,24 @@ export const indictmentCount = defineMessages({ description: 'Notaður sem skýritexti á "vínandamagn" svæði á ákæruliða skrefi í ákærum.', }, + speedingTitle: { + id: 'judicial.system.core:indictments_indictment.indictment_count.speeding', + defaultMessage: 'Hraði', + description: + 'Notaður sem titill á "Hraði" svæði á ákæruliða skrefi í ákærum.', + }, + recordedSpeedLabel: { + id: 'judicial.system.core:indictments_indictment.indictment_count.recorded_speed_label', + defaultMessage: 'Mældur hraði (km/klst)', + description: + 'Notaður sem titill á "Mældur hraði" innsláttarsvæði á ákæruliða skrefi í ákærum.', + }, + speedLimitLabel: { + id: 'judicial.system.core:indictments_indictment.indictment_count.speed_limit_label', + defaultMessage: 'Leyfilegur hámarkshraði (km/klst)', + description: + 'Notaður sem titill á "Leyfilegur hámarkshraði" innsláttarsvæði á ákæruliða skrefi í ákærum.', + }, lawsBrokenTitle: { id: 'judicial.system.core:indictments_indictment.indictment_count.laws_broken_title', defaultMessage: 'Lagaákvæði', @@ -118,10 +136,15 @@ export const indictmentCount = defineMessages({ defaultMessage: '{paragraph}. mgr. {article}. gr. umfl.', description: 'Notaður sem texti í lagaákvæði taggi.', }, + lawsBrokenTagArticleOnly: { + id: 'judicial.system.core:indictments_indictment.indictment_count.laws_broken_tag_article_only', + defaultMessage: '{article}. gr. umfl.', + description: 'Notaður sem texti í lagaákvæði taggi.', + }, incidentDescriptionAutofill: { - id: 'judicial.system.core:indictments_indictment.indictment_count.incident_description_auto_fill', + id: 'judicial.system.core:indictments_indictment.indictment_count.incident_description_auto_fill_v1', defaultMessage: - 'fyrir umferðarlagabrot með því að hafa, {incidentDate}, ekið bifreiðinni {vehicleRegistrationNumber} {reason} um {incidentLocation}, þar sem lögregla stöðvaði aksturinn.', + 'fyrir umferðarlagabrot með því að hafa, {incidentDate}, ekið bifreiðinni {vehicleRegistrationNumber} {reason} um {incidentLocation}, {isSpeeding, select, true {á {recordedSpeed} km hraða á klukkustund, að teknu tilliti til vikmarka, þar sem leyfður hámarkshraði var {speedLimit} km/klst} other {þar sem lögregla stöðvaði aksturinn}}.', description: 'Notaður sem skýritexti á "atvikalýsing" svæði á ákæruliða skrefi í umferðalagabrots ákærum.', }, diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.tsx index f5f5384a4f64..3cc742372fb2 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCount.tsx @@ -82,6 +82,12 @@ const offensesCompare = ( return 0 } +/** + * Indicates what laws are broken for each offence. The first number is + * the paragraph and the second is the article, i.e. [49, 2] means paragraph + * 49, article 2. If article is set to 0, that means that an article is + * not specified. + */ const offenseLawsMap: Record< IndictmentCountOffense | 'DRUNK_DRIVING_MINOR' | 'DRUNK_DRIVING_MAJOR', [number, number][] @@ -98,6 +104,7 @@ const offenseLawsMap: Record< [48, 1], [48, 2], ], + [IndictmentCountOffense.SPEEDING]: [[37, 0]], } const generalLaws: [number, number][] = [[95, 1]] @@ -267,11 +274,15 @@ export const getLegalArguments = ( } let articles = `${lawsBroken[0][1]}.` + const noArticle = articles === '0.' for (let i = 1; i < lawsBroken.length; i++) { let useSbr = true + if (lawsBroken[i][0] !== lawsBroken[i - 1][0]) { - articles = `${articles} mgr. ${lawsBroken[i - 1][0]}. gr.` + articles = `${noArticle ? '' : `${articles} mgr. `}${ + lawsBroken[i - 1][0] + }. gr.` useSbr = i > andIndex } @@ -334,11 +345,21 @@ export const getIncidentDescription = ( formatMessage, ) + const isSpeeding = indictmentCount.offenses?.includes( + IndictmentCountOffense.SPEEDING, + ) + + const recordedSpeed = indictmentCount.recordedSpeed ?? '[Mældur hraði]' + const speedLimit = indictmentCount.speedLimit ?? '[Leyfilegur hraði]' + return formatMessage(strings.incidentDescriptionAutofill, { incidentDate, vehicleRegistrationNumber: vehicleRegistration, reason, incidentLocation, + isSpeeding, + recordedSpeed, + speedLimit, }) } @@ -380,6 +401,10 @@ export const IndictmentCount: FC = ({ useState('') const [legalArgumentsErrorMessage, setLegalArgumentsErrorMessage] = useState('') + const [recordedSpeedErrorMessage, setRecordedSpeedErrorMessage] = + useState('') + const [speedLimitErrorMessage, setSpeedLimitErrorMessage] = + useState('') const subtypes: IndictmentSubtype[] = indictmentCount.policeCaseNumber ? workingCase.indictmentSubtypes[indictmentCount.policeCaseNumber] @@ -685,6 +710,10 @@ export const IndictmentCount: FC = ({ handleIndictmentCountChanges({ offenses, substances: indictmentCount.substances, + ...(offense === IndictmentCountOffense.SPEEDING && { + recordedSpeed: null, + speedLimit: null, + }), }) }} > @@ -767,6 +796,105 @@ export const IndictmentCount: FC = ({ )} + {indictmentCount.offenses?.includes( + IndictmentCountOffense.SPEEDING, + ) && ( + + + + { + const recordedSpeed = parseInt(event.target.value) + + removeErrorMessageIfValid( + ['empty'], + event.target.value, + recordedSpeedErrorMessage, + setRecordedSpeedErrorMessage, + ) + + updateIndictmentCountState( + indictmentCount.id, + { recordedSpeed }, + setWorkingCase, + ) + }} + onBlur={(event) => { + const recordedSpeed = parseInt(event.target.value) + + if (Number.isNaN(recordedSpeed)) { + setRecordedSpeedErrorMessage('Reitur má ekki vera tómur') + return + } + + handleIndictmentCountChanges({ + recordedSpeed, + }) + }} + > + + + + { + const speedLimit = parseInt(event.target.value) + + removeErrorMessageIfValid( + ['empty'], + event.target.value, + speedLimitErrorMessage, + setSpeedLimitErrorMessage, + ) + + updateIndictmentCountState( + indictmentCount.id, + { speedLimit }, + setWorkingCase, + ) + }} + onBlur={(event) => { + const speedLimit = parseInt(event.target.value) + + if (Number.isNaN(speedLimit)) { + setSpeedLimitErrorMessage('Reitur má ekki vera tómur') + return + } + + handleIndictmentCountChanges({ + speedLimit, + }) + }} + > + + + + )} {indictmentCount.offenses ?.filter( (offenseType) => diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCountEnum.strings.ts b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCountEnum.strings.ts index a7fe7e72c568..82c66b2f0a12 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCountEnum.strings.ts +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/IndictmentCountEnum.strings.ts @@ -21,4 +21,9 @@ export const indictmentCountEnum = defineMessages({ defaultMessage: 'Lyfjaakstur', description: 'Notaður sem titill á subtype fyrir "lyfjaakstur" brot.', }, + SPEEDING: { + id: 'judicial.system.core:indictments_indictment.indictment_count_enum.speeding', + defaultMessage: 'Hraðakstur', + description: 'Notaður sem titill á subtype fyrir "hraðakstur" brot.', + }, }) diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Substance/Substance.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Substance/Substance.tsx index 54f2f0d6749b..0bb15ac1b1f2 100644 --- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Substance/Substance.tsx +++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Indictment/Substance/Substance.tsx @@ -75,6 +75,6 @@ export const Substance: FC = ({ }} errorMessage={substanceAmountMissingErrorMessage} hasError={substanceAmountMissingErrorMessage !== ''} - > + /> ) } diff --git a/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/index.ts b/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/index.ts index 0d3e2e9a11c4..6f06420c38b8 100644 --- a/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/index.ts +++ b/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/index.ts @@ -128,11 +128,21 @@ const useIndictmentCounts = () => { ) const lawTag = useCallback( - (law: number[]) => - formatMessage(indictmentCount.lawsBrokenTag, { - paragraph: law[1], - article: law[0], - }), + (law: number[]) => { + const article = law[0] + const paragraph = law[1] + + if (paragraph === 0) { + return formatMessage(indictmentCount.lawsBrokenTagArticleOnly, { + article, + }) + } else { + return formatMessage(indictmentCount.lawsBrokenTag, { + article, + paragraph, + }) + } + }, [formatMessage], ) diff --git a/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/updateIndictmentCount.graphql b/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/updateIndictmentCount.graphql index f8aca1a9268d..a46263aaf9fd 100644 --- a/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/updateIndictmentCount.graphql +++ b/apps/judicial-system/web/src/utils/hooks/useIndictmentCounts/updateIndictmentCount.graphql @@ -12,5 +12,7 @@ mutation UpdateIndictmentCount($input: UpdateIndictmentCountInput!) { incidentDescription legalArguments indictmentCountSubtypes + recordedSpeed + speedLimit } } diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts index f53773ba08a3..0f8b8a8d5759 100644 --- a/apps/judicial-system/web/src/utils/validate.ts +++ b/apps/judicial-system/web/src/utils/validate.ts @@ -13,6 +13,7 @@ import { DateLog, DefenderChoice, IndictmentCount, + IndictmentCountOffense, IndictmentDecision, SessionArrangements, User, @@ -299,13 +300,20 @@ export const isIndictmentStepValid = (workingCase: Case): boolean => { return false } + const isValidSpeedingIndictmentCount = (indictmentCount: IndictmentCount) => + indictmentCount.offenses?.includes(IndictmentCountOffense.SPEEDING) + ? Boolean(indictmentCount.recordedSpeed) && + Boolean(indictmentCount.speedLimit) + : true + const isValidTrafficViolation = (indictmentCount: IndictmentCount) => Boolean(indictmentCount.policeCaseNumber) && Boolean(indictmentCount.offenses && indictmentCount.offenses?.length > 0) && Boolean(indictmentCount.vehicleRegistrationNumber) && Boolean(indictmentCount.lawsBroken) && Boolean(indictmentCount.incidentDescription) && - Boolean(indictmentCount.legalArguments) + Boolean(indictmentCount.legalArguments) && + isValidSpeedingIndictmentCount(indictmentCount) const isValidNonTrafficViolation = (indictmentCount: IndictmentCount) => Boolean(indictmentCount.incidentDescription) && diff --git a/libs/judicial-system/types/src/lib/indictmentCount.ts b/libs/judicial-system/types/src/lib/indictmentCount.ts index 2966f6cb686c..4dfbdac6189b 100644 --- a/libs/judicial-system/types/src/lib/indictmentCount.ts +++ b/libs/judicial-system/types/src/lib/indictmentCount.ts @@ -3,6 +3,7 @@ export enum IndictmentCountOffense { DRUNK_DRIVING = 'DRUNK_DRIVING', ILLEGAL_DRUGS_DRIVING = 'ILLEGAL_DRUGS_DRIVING', PRESCRIPTION_DRUGS_DRIVING = 'PRESCRIPTION_DRUGS_DRIVING', + SPEEDING = 'SPEEDING', } export enum Substance { @@ -98,4 +99,5 @@ export const offenseSubstances: { Substance.TRIAZOLAM, Substance.ZOLPIDEM, ], + [IndictmentCountOffense.SPEEDING]: [], }