Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions api/workAllocation/util.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -496,6 +497,47 @@ 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';
// eslint-disable-next-line dot-notation
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';
// eslint-disable-next-line dot-notation
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';
// eslint-disable-next-line dot-notation
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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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')}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -263,6 +264,84 @@ 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();

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';
// eslint-disable-next-line dot-notation
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';
// eslint-disable-next-line dot-notation
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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`;
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -73,4 +74,150 @@ 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', () => {
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', () => {
component.ngAfterViewInit();

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');

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(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'));
});

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);
});
});
});
Loading