From 0374383dc92d5c6a9b83a12da1fd0cdf24f58d0f Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 13 Nov 2025 13:52:57 +0000 Subject: [PATCH 01/10] updated toolkit version --- package.json | 2 +- yarn.lock | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index fe3bf72ca1..27bbf7b102 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@dr.pogodin/csurf": "^1.16.5", "@edium/fsm": "^2.1.2", "@faker-js/faker": "^9.2.0", - "@hmcts/ccd-case-ui-toolkit": "7.2.59", + "@hmcts/ccd-case-ui-toolkit": "link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit", "@hmcts/ccpay-web-component": "6.3.3", "@hmcts/frontend": "0.0.50-alpha", "@hmcts/media-viewer": "4.1.10", diff --git a/yarn.lock b/yarn.lock index 1d99db47cf..c1ef1d6eee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3285,14 +3285,11 @@ __metadata: languageName: node linkType: hard -"@hmcts/ccd-case-ui-toolkit@npm:7.2.59": - version: 7.2.59 - resolution: "@hmcts/ccd-case-ui-toolkit@npm:7.2.59" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/e7928663436e238c520d4aef8b17f16ad96be6675ac534bd98910e9f43311a0512274dbec592426c0295824dbe23928c5705f2711a30a62b909584a0c7494b57 +"@hmcts/ccd-case-ui-toolkit@link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit::locator=rpx-exui%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@hmcts/ccd-case-ui-toolkit@link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit::locator=rpx-exui%40workspace%3A." languageName: node - linkType: hard + linkType: soft "@hmcts/ccpay-web-component@npm:6.3.3": version: 6.3.3 @@ -21834,7 +21831,7 @@ __metadata: "@dr.pogodin/csurf": "npm:^1.16.5" "@edium/fsm": "npm:^2.1.2" "@faker-js/faker": "npm:^9.2.0" - "@hmcts/ccd-case-ui-toolkit": "npm:7.2.59" + "@hmcts/ccd-case-ui-toolkit": "link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit" "@hmcts/ccpay-web-component": "npm:6.3.3" "@hmcts/frontend": "npm:0.0.50-alpha" "@hmcts/media-viewer": "npm:4.1.10" From f10ccd30ea7901f2e55b02b493603999c7f37d66 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 10:05:33 +0000 Subject: [PATCH 02/10] refactored manual getTimezoneOffset calculations to use momentjs --- api/workAllocation/util.ts | 7 +--- .../hearing-actual-summary.component.ts | 11 ++++-- .../hearing-actuals-summary-base.component.ts | 6 +-- .../datetime/noc-datetime-field.component.ts | 37 +++++++++++++------ .../services/duration-helper.service.spec.ts | 9 +++-- .../services/duration-helper.service.ts | 3 +- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/api/workAllocation/util.ts b/api/workAllocation/util.ts index d7127b20c8..e7f3616493 100644 --- a/api/workAllocation/util.ts +++ b/api/workAllocation/util.ts @@ -1,5 +1,6 @@ import { AxiosResponse } from 'axios'; import * as express from 'express'; +import * as moment from 'moment'; import { getConfigValue } from '../configuration'; import { CASEWORKER_PAGE_SIZE, SERVICES_CCD_DATA_STORE_API_PATH, SERVICES_ROLE_ASSIGNMENT_API_PATH } from '../configuration/references'; @@ -918,11 +919,7 @@ export function getAccessStatus(roleAssignment: RoleAssignment): boolean { } export function formatDate(date: Date) { - date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); - const day = date.toLocaleString('default', { day: '2-digit' }); - const month = date.toLocaleString('default', { month: 'short' }); - const year = date.toLocaleString('default', { year: 'numeric' }); - return `${day} ${month} ${year}`; + return moment(date).format('DD MMM YYYY'); } export function getAccessType(roleAssignment: RoleAssignment) { diff --git a/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.ts b/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.ts index 2a4eb386f5..3d12881d06 100644 --- a/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.ts +++ b/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.ts @@ -56,8 +56,8 @@ export class HearingActualSummaryComponent implements OnInit { this.hearingActualsMainModel?.hearingActuals?.actualHearingDays[0]?.hearingDate; } - private convertUTCDateToLocalDate(date): Date { - return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); + private convertUTCDateToLocalDate(utcDateTimeString): moment.Moment { + return moment.utc(utcDateTimeString).local(); } private applyActualsModel(): void { @@ -80,6 +80,11 @@ export class HearingActualSummaryComponent implements OnInit { } public actualMultiDaysHearingDates(): string { - return `${moment.tz(this.convertUTCDateToLocalDate(new Date(this.hearingActualsMainModel?.hearingActuals?.actualHearingDays[0].hearingStartTime)), moment.tz.guess()).format('DD MMM YYYY')} - ${moment.tz(this.convertUTCDateToLocalDate(new Date(this.hearingActualsMainModel?.hearingActuals?.actualHearingDays[this.hearingActualsMainModel?.hearingActuals?.actualHearingDays.length - 1].hearingStartTime)), moment.tz.guess()).format('DD MMM YYYY')}`; + const firstDay = this.hearingActualsMainModel?.hearingActuals?.actualHearingDays[0].hearingStartTime; + const lastDay = this.hearingActualsMainModel?.hearingActuals?.actualHearingDays[ + this.hearingActualsMainModel?.hearingActuals?.actualHearingDays.length - 1 + ].hearingStartTime; + + return `${this.convertUTCDateToLocalDate(firstDay).format('DD MMM YYYY')} - ${this.convertUTCDateToLocalDate(lastDay).format('DD MMM YYYY')}`; } } diff --git a/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.ts b/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.ts index fc32cb1299..bc6fbadeaa 100644 --- a/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.ts +++ b/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.ts @@ -145,7 +145,7 @@ export class HearingActualsSummaryBaseComponent implements OnInit, OnDestroy { } public calculateEarliestHearingDate(hearingDays: ActualHearingDayModel[]): string { - const moments: moment.Moment[] = hearingDays.map((d) => moment.tz(this.convertUTCDateToLocalDate(new Date(d.hearingStartTime)), moment.tz.guess())); + const moments: moment.Moment[] = hearingDays.map((d) => this.convertUTCDateToLocalDate(d.hearingStartTime)); if (moments.length > 1) { return `${moment.min(moments).format('DD MMM YYYY')} - ${moment.max(moments).format('DD MMM YYYY')}`; } @@ -157,8 +157,8 @@ export class HearingActualsSummaryBaseComponent implements OnInit, OnDestroy { return this.getTime(ActualHearingsUtils.getPauseDateTime(day, state)); } - private convertUTCDateToLocalDate(date): Date { - return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); + private convertUTCDateToLocalDate(utcDateTimeString): moment.Moment { + return moment.utc(utcDateTimeString).local(); } // Convert UTC date/time string to a time string in the specified time zone and format using ccdDatePipe diff --git a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.ts b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.ts index 11bc5120b5..688c3f1c3a 100644 --- a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.ts +++ b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.ts @@ -1,5 +1,6 @@ import { AfterViewInit, Component, OnInit } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import * as moment from 'moment'; import { AppUtils } from '../../../../app/app-utils'; import { AbstractFieldWriteComponent } from '../abstract-field-write.component'; @@ -27,17 +28,18 @@ export class NocDateTimeFieldComponent extends AbstractFieldWriteComponent imple second: [null, Validators.required] }); if (this.datetimeControl.value) { - const [datePart, timePart] = this.datetimeControl.value.split('T'); - const dateValues = datePart.split('-'); - this.datetimeGroup.controls.year.setValue(dateValues[0] || ''); - this.datetimeGroup.controls.month.setValue(dateValues[1] || ''); - this.datetimeGroup.controls.day.setValue(dateValues[2] || ''); - if (timePart) { - const timeParts = timePart.replace('.000', '').split(':'); - this.datetimeGroup.controls.hour.setValue(timeParts[0] || ''); - this.datetimeGroup.controls.minute.setValue(timeParts[1] || ''); - this.datetimeGroup.controls.second.setValue(timeParts[2] || ''); - } + // convert UTC datetime to local time for display + const utcValue = this.datetimeControl.value; + const utcMoment = moment.utc(utcValue); + const localMoment = utcMoment.local(); + + // extract local date and time components + this.datetimeGroup.controls.year.setValue(localMoment.year().toString()); + this.datetimeGroup.controls.month.setValue(AppUtils.pad((localMoment.month() + 1).toString())); + this.datetimeGroup.controls.day.setValue(AppUtils.pad(localMoment.date().toString())); + this.datetimeGroup.controls.hour.setValue(AppUtils.pad(localMoment.hours().toString())); + this.datetimeGroup.controls.minute.setValue(AppUtils.pad(localMoment.minutes().toString())); + this.datetimeGroup.controls.second.setValue(AppUtils.pad(localMoment.seconds().toString())); } } @@ -53,7 +55,18 @@ export class NocDateTimeFieldComponent extends AbstractFieldWriteComponent imple this.datetimeGroup.value.minute !== null ? AppUtils.pad(this.datetimeGroup.value.minute) : '', this.datetimeGroup.value.second !== null ? AppUtils.pad(this.datetimeGroup.value.second) : '' ].join(':'); - this.datetimeControl.setValue(`${date}T${time}.000`); + const localDateTimeString = `${date}T${time}.000`; + + // convert local time to UTC datetime for storage + const localMoment = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); + if (localMoment.isValid()) { + const utcMoment = localMoment.utc(); + const utcValue = utcMoment.format('YYYY-MM-DDTHH:mm:ss.SSS'); + this.datetimeControl.setValue(utcValue); + } else { + // set the local datetime string to be caught by validation + this.datetimeControl.setValue(localDateTimeString); + } }); } diff --git a/src/role-access/services/duration-helper.service.spec.ts b/src/role-access/services/duration-helper.service.spec.ts index b2e506b748..870a64e8e3 100644 --- a/src/role-access/services/duration-helper.service.spec.ts +++ b/src/role-access/services/duration-helper.service.spec.ts @@ -206,10 +206,13 @@ describe('DurationHelperService', () => { expect(result).toBe(null); }); - it('should return current date if date is current passed', () => { - const date = new Date(); + it('should return a Date object when a valid date is passed', () => { + const date = new Date('2025-06-01T15:00:00'); + date.setMilliseconds(0); const result = durationHelperService.setUTCTimezone(date); - expect(result).toBe(date); + expect(result.getUTCHours()).toBe(date.getHours()); + expect(result.getUTCMinutes()).toBe(date.getMinutes()); + expect(result.getUTCSeconds()).toBe(date.getSeconds()); }); it('should return correct JSON date if date is passed', () => { diff --git a/src/role-access/services/duration-helper.service.ts b/src/role-access/services/duration-helper.service.ts index 61b4127b54..3d37aa13ab 100644 --- a/src/role-access/services/duration-helper.service.ts +++ b/src/role-access/services/duration-helper.service.ts @@ -141,8 +141,7 @@ export class DurationHelperService { if (!date) { return null; } - date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); - return date; + return moment.utc(moment(date).format('YYYY-MM-DDTHH:mm:ss')).toDate(); } public setStartTimeOfDay(date: Date): Date { From 83da4087e5152c75eeef65436abec11f193a26cd Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 10:16:56 +0000 Subject: [PATCH 03/10] updated toolkit version --- package.json | 2 +- yarn.lock | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7cb788c3ca..c43127ea2f 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@dr.pogodin/csurf": "^1.16.5", "@edium/fsm": "^2.1.2", "@faker-js/faker": "^9.2.0", - "@hmcts/ccd-case-ui-toolkit": "link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit", + "@hmcts/ccd-case-ui-toolkit": "7.2.59-exui-3066-2", "@hmcts/ccpay-web-component": "6.3.3", "@hmcts/frontend": "0.0.50-alpha", "@hmcts/media-viewer": "4.1.10", diff --git a/yarn.lock b/yarn.lock index d478510650..d93a49041c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3519,11 +3519,14 @@ __metadata: languageName: node linkType: hard -"@hmcts/ccd-case-ui-toolkit@link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit::locator=rpx-exui%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@hmcts/ccd-case-ui-toolkit@link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit::locator=rpx-exui%40workspace%3A." +"@hmcts/ccd-case-ui-toolkit@npm:7.2.59-exui-3066-2": + version: 7.2.59-exui-3066-2 + resolution: "@hmcts/ccd-case-ui-toolkit@npm:7.2.59-exui-3066-2" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/6db63b303074d69dc19990ec7d32a0bc805c1c4a7e2b6fcaf7cc3f46d5022cb00b665a53255bdb01a061f86d0eda23fe43bf546aab39d4cb3651108cc6c7dd69 languageName: node - linkType: soft + linkType: hard "@hmcts/ccpay-web-component@npm:6.3.3": version: 6.3.3 @@ -23401,7 +23404,7 @@ __metadata: "@dr.pogodin/csurf": "npm:^1.16.5" "@edium/fsm": "npm:^2.1.2" "@faker-js/faker": "npm:^9.2.0" - "@hmcts/ccd-case-ui-toolkit": "link:../ccd-case-ui-toolkit/dist/ccd-case-ui-toolkit" + "@hmcts/ccd-case-ui-toolkit": "npm:7.2.59-exui-3066-2" "@hmcts/ccpay-web-component": "npm:6.3.3" "@hmcts/frontend": "npm:0.0.50-alpha" "@hmcts/media-viewer": "npm:4.1.10" From c2514e4e0b7ee24ac07499882740eefd1bf85386 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 10:26:55 +0000 Subject: [PATCH 04/10] updated yarn-audit-known-issues --- yarn-audit-known-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 224bc58b9b..c5708aa50d 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -9,7 +9,7 @@ {"value":"csurf","children":{"ID":"csurf (deprecation)","Issue":"This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions","Severity":"moderate","Vulnerable Versions":"1.11.0","Tree Versions":["1.11.0"],"Dependents":["@hmcts/rpx-xui-node-lib@virtual:478250b179e2f7a41962cb81e8de022adafb1a3a18c5c9a01a14fbfc1b28d5290463c48c9e2b547a1f1c34dc9b7b468a7fcd7685a99bff9367385d59331a4cd4#npm:2.30.7-cleanup-logs"]}} {"value":"domexception","children":{"ID":"domexception (deprecation)","Issue":"Use your platform's native DOMException instead","Severity":"moderate","Vulnerable Versions":"4.0.0","Tree Versions":["4.0.0"],"Dependents":["jsdom@virtual:145e7af5a4eef7edc3b5342155c9759e46fd272a65da8c54e71e3a726711ad979907e1887a3fdcf00ecab676547214a60ab3eeb7f8437d187eab031c26d7a1bb#npm:20.0.3"]}} {"value":"fstream","children":{"ID":"fstream (deprecation)","Issue":"This package is no longer supported.","Severity":"moderate","Vulnerable Versions":"1.0.12","Tree Versions":["1.0.12"],"Dependents":["unzipper@npm:0.10.14"]}} -{"value":"glob","children":{"ID":1109809,"Issue":"glob CLI: Command injection via -c/--cmd executes matches with shell:true","URL":"https://github.com/advisories/GHSA-5j98-mcp5-4vw2","Severity":"high","Vulnerable Versions":">=10.3.7 <=11.0.3","Tree Versions":["10.4.5"],"Dependents":["jest-runtime@npm:30.1.3"]}} +{"value":"glob","children":{"ID":1109842,"Issue":"glob CLI: Command injection via -c/--cmd executes matches with shell:true","URL":"https://github.com/advisories/GHSA-5j98-mcp5-4vw2","Severity":"high","Vulnerable Versions":">=10.2.0 <10.5.0","Tree Versions":["10.4.5"],"Dependents":["jest-runtime@npm:30.1.3"]}} {"value":"glob","children":{"ID":"glob (deprecation)","Issue":"Glob versions prior to v9 are no longer supported","Severity":"moderate","Vulnerable Versions":"7.2.3","Tree Versions":["7.2.3"],"Dependents":["shelljs@npm:0.8.5"]}} {"value":"got","children":{"ID":1088948,"Issue":"Got allows a redirect to a UNIX socket","URL":"https://github.com/advisories/GHSA-pfrx-2q88-qq97","Severity":"moderate","Vulnerable Versions":"<11.8.5","Tree Versions":["9.6.0"],"Dependents":["openid-client@npm:3.15.10"]}} {"value":"govuk-elements-sass","children":{"ID":"govuk-elements-sass (deprecation)","Issue":"GOV.UK Elements is no longer maintained. Use the GOV.UK Design System instead: https://frontend.design-system.service.gov.uk/v4/migrating-from-legacy-products/","Severity":"moderate","Vulnerable Versions":"3.1.3","Tree Versions":["3.1.3"],"Dependents":["rpx-exui@workspace:."]}} From 6f2d6392d510b45f867676238b8c73c10abd82e7 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 12:15:30 +0000 Subject: [PATCH 05/10] added unit tests for hearing-actual-summary.component --- .../hearing-actual-summary.component.spec.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts b/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts index 83467c9d1a..8bc78f884d 100644 --- a/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts +++ b/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts @@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { Store } from '@ngrx/store'; import { provideMockStore } from '@ngrx/store/testing'; import { Observable, of } from 'rxjs'; +import * as moment from 'moment'; import { MockRpxTranslatePipe } from '../../../app/shared/test/mock-rpx-translate.pipe'; import { hearingActualsMainModel, initialState } from '../../hearing.test.data'; import { HearingResult } from '../../models/hearings.enum'; @@ -496,6 +497,44 @@ describe('HearingActualSummaryComponent', () => { expect(component.hearingTypeDescription).toEqual(''); }); + describe('convertUTCDateToLocalDate', () => { + it('should convert UTC datetime string to local moment object', () => { + const utcString = '2025-04-18T20:20:24.976537'; + const result = component['convertUTCDateToLocalDate'](utcString); + const expected = moment.utc(utcString).local(); + + expect(result).toBeDefined(); + expect(moment.isMoment(result)).toBe(true); + expect(result.isValid()).toBe(true); + expect(result.isSame(expected)).toBe(true); + expect(result.format('YYYY-MM-DDTHH:mm:ss')).toEqual(expected.format('YYYY-MM-DDTHH:mm:ss')); + }); + + it('should handle UTC datetime with Z suffix', () => { + const utcString = '2025-04-17T16:14:53.844Z'; + const result = component['convertUTCDateToLocalDate'](utcString); + const expected = moment.utc(utcString).local(); + + expect(result).toBeDefined(); + expect(moment.isMoment(result)).toBe(true); + expect(result.isValid()).toBe(true); + expect(result.isSame(expected)).toBe(true); + expect(result.format('YYYY-MM-DDTHH:mm:ss')).toEqual(expected.format('YYYY-MM-DDTHH:mm:ss')); + }); + + it('should handle UTC datetime without Z suffix', () => { + const utcString = '2025-04-17T16:16:43.548'; + const result = component['convertUTCDateToLocalDate'](utcString); + const expected = moment.utc(utcString).local(); + + expect(result).toBeDefined(); + expect(moment.isMoment(result)).toBe(true); + expect(result.isValid()).toBe(true); + expect(result.isSame(expected)).toBe(true); + expect(result.format('YYYY-MM-DDTHH:mm:ss')).toEqual(expected.format('YYYY-MM-DDTHH:mm:ss')); + }); + }); + afterEach(() => { fixture.destroy(); }); From dc6651e309cefae600d289fa6ae0a06b1cb65237 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 12:26:27 +0000 Subject: [PATCH 06/10] added unit tests for hearing-actuals-summary-base.component --- ...ing-actuals-summary-base.component.spec.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts b/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts index b9c5779a35..c31d643225 100644 --- a/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts +++ b/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts @@ -5,6 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; import * as _ from 'lodash'; import { Observable, of } from 'rxjs'; +import * as moment from 'moment'; import { hearingActualsMainModel, hearingStageRefData, initialState, partyChannelsRefData, partySubChannelsRefData } from '../../../hearing.test.data'; import { ActualHearingDayModel } from '../../../models/hearingActualsMainModel'; import { ACTION } from '../../../models/hearings.enum'; @@ -263,6 +264,81 @@ describe('HearingActualsSummaryBaseComponent', () => { }); }); + describe('convertUTCDateToLocalDate', () => { + it('should convert UTC datetime string to local moment object', () => { + const utcString = '2025-04-18T20:20:24.976537'; + const result = component['convertUTCDateToLocalDate'](utcString); + const expected = moment.utc(utcString).local(); + + expect(moment.isMoment(result)).toBe(true); + expect(result.isValid()).toBe(true); + expect(result.isSame(expected)).toBe(true); + expect(result.format('YYYY-MM-DDTHH:mm:ss')).toEqual(expected.format('YYYY-MM-DDTHH:mm:ss')); + }); + + it('should handle UTC datetime with Z suffix', () => { + const utcString = '2025-11-17T16:14:53.844Z'; + const result = component['convertUTCDateToLocalDate'](utcString); + const expected = moment.utc(utcString).local(); + + expect(result.format('YYYY-MM-DDTHH:mm:ss')).toEqual(expected.format('YYYY-MM-DDTHH:mm:ss')); + }); + + it('should handle UTC datetime without Z suffix', () => { + const utcString = '2025-11-17T16:16:43.548'; + const result = component['convertUTCDateToLocalDate'](utcString); + const expected = moment.utc(utcString).local(); + + expect(result.format('YYYY-MM-DDTHH:mm:ss')).toEqual(expected.format('YYYY-MM-DDTHH:mm:ss')); + }); + }); + + describe('calculateEarliestHearingDate', () => { + it('should return formatted date range for multiple hearing days', () => { + const hearingDays: ActualHearingDayModel[] = [ + { + hearingDate: '2025-03-12', + hearingStartTime: '2025-03-12T09:00:00.000Z', + hearingEndTime: '2025-03-12T17:00:00.000Z', + pauseDateTimes: [], + notRequired: false, + actualDayParties: [] + }, + { + hearingDate: '2025-03-14', + hearingStartTime: '2025-03-14T09:00:00.000Z', + hearingEndTime: '2025-03-14T17:00:00.000Z', + pauseDateTimes: [], + notRequired: false, + actualDayParties: [] + } + ]; + + const result = component.calculateEarliestHearingDate(hearingDays); + expect(result).toBeDefined(); + expect(typeof result).toBe('string'); + expect(result).toBe('12 Mar 2025 - 14 Mar 2025'); + }); + + it('should return single date for one hearing day', () => { + const hearingDays: ActualHearingDayModel[] = [ + { + hearingDate: '2025-03-12', + hearingStartTime: '2025-03-12T09:00:00.000Z', + hearingEndTime: '2025-03-12T17:00:00.000Z', + pauseDateTimes: [], + notRequired: false, + actualDayParties: [] + } + ]; + + const result = component.calculateEarliestHearingDate(hearingDays); + expect(result).toBeDefined(); + expect(typeof result).toBe('string'); + expect(result).toBe('12 Mar 2025'); + }); + }); + afterEach(() => { fixture.destroy(); }); From 421e9fb052b0ebd2e487872163ea71c7ea4e7fff Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 12:38:51 +0000 Subject: [PATCH 07/10] add unit tests for noc-datetime-field.component --- .../noc-datetime-field.component.spec.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts index 1f1c0db3fc..8c96d62a38 100644 --- a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts +++ b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { provideMockStore } from '@ngrx/store/testing'; import { of } from 'rxjs'; +import * as moment from 'moment'; import { UtilsModule } from '../utils/utils.module'; import { NocDateTimeFieldComponent } from './noc-datetime-field.component'; @@ -73,4 +74,62 @@ describe('NocDateTimeFieldComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('UTC to local conversion on initialization', () => { + it('should convert UTC datetime to local time for display', () => { + // set a UTC datetime value + const utcValue = '2022-07-15T14:30:00.000'; + component.datetimeControl.setValue(utcValue); + + // manually trigger ngOnInit logic + const utcMoment = moment.utc(utcValue); + const localMoment = utcMoment.local(); + + component.datetimeGroup.controls.year.setValue(localMoment.year().toString()); + component.datetimeGroup.controls.month.setValue((localMoment.month() + 1).toString().padStart(2, '0')); + component.datetimeGroup.controls.day.setValue(localMoment.date().toString().padStart(2, '0')); + component.datetimeGroup.controls.hour.setValue(localMoment.hours().toString().padStart(2, '0')); + component.datetimeGroup.controls.minute.setValue(localMoment.minutes().toString().padStart(2, '0')); + component.datetimeGroup.controls.second.setValue(localMoment.seconds().toString().padStart(2, '0')); + + // verify the form group contains local time values + expect(component.datetimeGroup.controls.year.value).toBe(localMoment.year().toString()); + expect(component.datetimeGroup.controls.month.value).toBe((localMoment.month() + 1).toString().padStart(2, '0')); + expect(component.datetimeGroup.controls.day.value).toBe(localMoment.date().toString().padStart(2, '0')); + expect(component.datetimeGroup.controls.hour.value).toBe(localMoment.hours().toString().padStart(2, '0')); + expect(component.datetimeGroup.controls.minute.value).toBe(localMoment.minutes().toString().padStart(2, '0')); + expect(component.datetimeGroup.controls.second.value).toBe(localMoment.seconds().toString().padStart(2, '0')); + }); + }); + + describe('Local to UTC conversion on value change', () => { + it('should convert local datetime to UTC for storage', () => { + // set up the valueChanges subscription + component.ngAfterViewInit(); + + // set local datetime values in the form + component.datetimeGroup.controls.year.setValue('2025'); + component.datetimeGroup.controls.month.setValue('04'); + component.datetimeGroup.controls.day.setValue('15'); + component.datetimeGroup.controls.hour.setValue('14'); + component.datetimeGroup.controls.minute.setValue('30'); + component.datetimeGroup.controls.second.setValue('45'); + + // construct expected values for verification + const localDateTimeString = '2025-04-15T14:30:45.000'; + const localMoment = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); + const expectedUtcValue = localMoment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'); + + // verify the automatic conversion happened correctly + expect(component.datetimeControl.value).toBeDefined(); + expect(component.datetimeControl.value).toBe(expectedUtcValue); + expect(component.datetimeControl.value).toBe('2025-04-15T13:30:45.000'); + expect(moment.utc(component.datetimeControl.value).isValid()).toBe(true); + + // verify the UTC value converts back to the original local time + const convertedBackToLocal = moment.utc(component.datetimeControl.value).local(); + const originalLocalTime = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); + expect(convertedBackToLocal.format('YYYY-MM-DDTHH:mm:ss')).toBe(originalLocalTime.format('YYYY-MM-DDTHH:mm:ss')); + }); + }); }); From 92a96e6d0aa12c10b563d5b487c2ae756a0594a2 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 12:43:42 +0000 Subject: [PATCH 08/10] linting fixes --- .../hearing-actual-summary.component.spec.ts | 3 +++ .../hearing-actuals-summary-base.component.spec.ts | 3 +++ .../noc-field/datetime/noc-datetime-field.component.spec.ts | 6 +----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts b/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts index 8bc78f884d..5f40c98a8a 100644 --- a/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts +++ b/src/hearings/components/hearing-actual-summary/hearing-actual-summary.component.spec.ts @@ -500,6 +500,7 @@ describe('HearingActualSummaryComponent', () => { describe('convertUTCDateToLocalDate', () => { it('should convert UTC datetime string to local moment object', () => { const utcString = '2025-04-18T20:20:24.976537'; + // eslint-disable-next-line dot-notation const result = component['convertUTCDateToLocalDate'](utcString); const expected = moment.utc(utcString).local(); @@ -512,6 +513,7 @@ describe('HearingActualSummaryComponent', () => { it('should handle UTC datetime with Z suffix', () => { const utcString = '2025-04-17T16:14:53.844Z'; + // eslint-disable-next-line dot-notation const result = component['convertUTCDateToLocalDate'](utcString); const expected = moment.utc(utcString).local(); @@ -524,6 +526,7 @@ describe('HearingActualSummaryComponent', () => { it('should handle UTC datetime without Z suffix', () => { const utcString = '2025-04-17T16:16:43.548'; + // eslint-disable-next-line dot-notation const result = component['convertUTCDateToLocalDate'](utcString); const expected = moment.utc(utcString).local(); diff --git a/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts b/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts index c31d643225..1f46f1547a 100644 --- a/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts +++ b/src/hearings/containers/hearing-actuals/hearing-actuals-summary-base/hearing-actuals-summary-base.component.spec.ts @@ -267,6 +267,7 @@ describe('HearingActualsSummaryBaseComponent', () => { describe('convertUTCDateToLocalDate', () => { it('should convert UTC datetime string to local moment object', () => { const utcString = '2025-04-18T20:20:24.976537'; + // eslint-disable-next-line dot-notation const result = component['convertUTCDateToLocalDate'](utcString); const expected = moment.utc(utcString).local(); @@ -278,6 +279,7 @@ describe('HearingActualsSummaryBaseComponent', () => { it('should handle UTC datetime with Z suffix', () => { const utcString = '2025-11-17T16:14:53.844Z'; + // eslint-disable-next-line dot-notation const result = component['convertUTCDateToLocalDate'](utcString); const expected = moment.utc(utcString).local(); @@ -286,6 +288,7 @@ describe('HearingActualsSummaryBaseComponent', () => { it('should handle UTC datetime without Z suffix', () => { const utcString = '2025-11-17T16:16:43.548'; + // eslint-disable-next-line dot-notation const result = component['convertUTCDateToLocalDate'](utcString); const expected = moment.utc(utcString).local(); diff --git a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts index 8c96d62a38..c83b390b39 100644 --- a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts +++ b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts @@ -77,7 +77,6 @@ describe('NocDateTimeFieldComponent', () => { describe('UTC to local conversion on initialization', () => { it('should convert UTC datetime to local time for display', () => { - // set a UTC datetime value const utcValue = '2022-07-15T14:30:00.000'; component.datetimeControl.setValue(utcValue); @@ -104,10 +103,8 @@ describe('NocDateTimeFieldComponent', () => { describe('Local to UTC conversion on value change', () => { it('should convert local datetime to UTC for storage', () => { - // set up the valueChanges subscription component.ngAfterViewInit(); - // set local datetime values in the form component.datetimeGroup.controls.year.setValue('2025'); component.datetimeGroup.controls.month.setValue('04'); component.datetimeGroup.controls.day.setValue('15'); @@ -115,7 +112,6 @@ describe('NocDateTimeFieldComponent', () => { component.datetimeGroup.controls.minute.setValue('30'); component.datetimeGroup.controls.second.setValue('45'); - // construct expected values for verification const localDateTimeString = '2025-04-15T14:30:45.000'; const localMoment = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); const expectedUtcValue = localMoment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'); @@ -125,7 +121,7 @@ describe('NocDateTimeFieldComponent', () => { expect(component.datetimeControl.value).toBe(expectedUtcValue); expect(component.datetimeControl.value).toBe('2025-04-15T13:30:45.000'); expect(moment.utc(component.datetimeControl.value).isValid()).toBe(true); - + // verify the UTC value converts back to the original local time const convertedBackToLocal = moment.utc(component.datetimeControl.value).local(); const originalLocalTime = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); From 68b9c10ec6e97f3d27d1fd3195c67a2ca9bbbc94 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 13:49:16 +0000 Subject: [PATCH 09/10] added unit tests to fix sonar coverage --- .../noc-datetime-field.component.spec.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts index c83b390b39..fed8a34c33 100644 --- a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts +++ b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts @@ -127,5 +127,98 @@ describe('NocDateTimeFieldComponent', () => { const originalLocalTime = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); expect(convertedBackToLocal.format('YYYY-MM-DDTHH:mm:ss')).toBe(originalLocalTime.format('YYYY-MM-DDTHH:mm:ss')); }); + + it('should handle invalid datetime and set raw value for validation', () => { + component.ngAfterViewInit(); + + component.datetimeGroup.controls.year.setValue('2025'); + component.datetimeGroup.controls.month.setValue('13'); // invalid month + component.datetimeGroup.controls.day.setValue('32'); // invalid day + component.datetimeGroup.controls.hour.setValue('25'); // invalid hour + component.datetimeGroup.controls.minute.setValue('60'); // invalid minute + component.datetimeGroup.controls.second.setValue('60'); // invalid second + + const invalidString = '2025-13-32T25:60:60.000'; + const invalidMoment = moment(invalidString, 'YYYY-MM-DDTHH:mm:ss.SSS'); + + expect(invalidMoment.isValid()).toBe(false); + expect(component.datetimeControl.value).toBe(invalidString); + }); + + it('should pad single digit values correctly', () => { + component.ngAfterViewInit(); + + component.datetimeGroup.controls.year.setValue('2025'); + component.datetimeGroup.controls.month.setValue('4'); // single digit + component.datetimeGroup.controls.day.setValue('5'); // single digit + component.datetimeGroup.controls.hour.setValue('9'); // single digit + component.datetimeGroup.controls.minute.setValue('8'); // single digit + component.datetimeGroup.controls.second.setValue('7'); // single digit + + const localDateTimeString = '2025-04-05T09:08:07.000'; + const localMoment = moment(localDateTimeString, 'YYYY-MM-DDTHH:mm:ss.SSS'); + const expectedUtcValue = localMoment.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'); + + expect(component.datetimeControl.value).toBe(expectedUtcValue); + }); + + it('should handle midnight correctly (00:00:00)', () => { + component.ngAfterViewInit(); + + component.datetimeGroup.controls.year.setValue('2025'); + component.datetimeGroup.controls.month.setValue('01'); + component.datetimeGroup.controls.day.setValue('01'); + component.datetimeGroup.controls.hour.setValue('0'); + component.datetimeGroup.controls.minute.setValue('0'); + component.datetimeGroup.controls.second.setValue('0'); + + const localMidnight = moment('2025-01-01T00:00:00.000', 'YYYY-MM-DDTHH:mm:ss.SSS'); + const expectedUtcValue = localMidnight.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'); + + expect(component.datetimeControl.value).toBe(expectedUtcValue); + expect(moment.utc(component.datetimeControl.value).isValid()).toBe(true); + }); + + it('should handle end of day correctly (23:59:59)', () => { + component.ngAfterViewInit(); + + component.datetimeGroup.controls.year.setValue('2025'); + component.datetimeGroup.controls.month.setValue('12'); + component.datetimeGroup.controls.day.setValue('31'); + component.datetimeGroup.controls.hour.setValue('23'); + component.datetimeGroup.controls.minute.setValue('59'); + component.datetimeGroup.controls.second.setValue('59'); + + const localEndOfDay = moment('2025-12-31T23:59:59.000', 'YYYY-MM-DDTHH:mm:ss.SSS'); + const expectedUtcValue = localEndOfDay.utc().format('YYYY-MM-DDTHH:mm:ss.SSS'); + + expect(component.datetimeControl.value).toBe(expectedUtcValue); + expect(moment.utc(component.datetimeControl.value).isValid()).toBe(true); + }); + + it('should properly build datetime string from form values', () => { + component.ngAfterViewInit(); + + component.datetimeGroup.controls.year.setValue('2025'); + component.datetimeGroup.controls.month.setValue('7'); + component.datetimeGroup.controls.day.setValue('15'); + component.datetimeGroup.controls.hour.setValue('14'); + component.datetimeGroup.controls.minute.setValue('30'); + component.datetimeGroup.controls.second.setValue('0'); + + // the internal string building should create: 2025-07-15T14:30:00.000 + const utcValue = component.datetimeControl.value; + expect(utcValue).toBeDefined(); + expect(moment.utc(utcValue).isValid()).toBe(true); + + // verify it converts back to the same local time + const backToLocal = moment.utc(utcValue).local(); + expect(backToLocal.year()).toBe(2025); + expect(backToLocal.month()).toBe(6); // 0-indexed + expect(backToLocal.date()).toBe(15); + expect(backToLocal.hours()).toBe(14); + expect(backToLocal.minutes()).toBe(30); + expect(backToLocal.seconds()).toBe(0); + }); }); }); From 13236f73d31446b1daad39c2189b4e860fabef73 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 19 Nov 2025 14:01:30 +0000 Subject: [PATCH 10/10] tidied up code --- .../noc-field/datetime/noc-datetime-field.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts index fed8a34c33..257fd96047 100644 --- a/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts +++ b/src/noc/containers/noc-field/datetime/noc-datetime-field.component.spec.ts @@ -119,7 +119,6 @@ describe('NocDateTimeFieldComponent', () => { // verify the automatic conversion happened correctly expect(component.datetimeControl.value).toBeDefined(); expect(component.datetimeControl.value).toBe(expectedUtcValue); - expect(component.datetimeControl.value).toBe('2025-04-15T13:30:45.000'); expect(moment.utc(component.datetimeControl.value).isValid()).toBe(true); // verify the UTC value converts back to the original local time