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
3 changes: 3 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## RELEASE NOTES

### Version 7.2.59-exui-3066-2
**EXUI-3066** HALO-23226 Dates/Times: Accept input in local time

### Version 7.2.59
**EXUI-3661** Performance issues on screens to return data
**EXUI-3681** Users unable to upload documents
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hmcts/ccd-case-ui-toolkit",
"version": "7.2.59",
"version": "7.2.59-exui-3066-2",
"engines": {
"node": ">=18.19.0"
},
Expand Down
2 changes: 1 addition & 1 deletion projects/ccd-case-ui-toolkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hmcts/ccd-case-ui-toolkit",
"version": "7.2.59",
"version": "7.2.59-exui-3066-2",
"engines": {
"node": ">=18.19.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import moment from 'moment';

@Component({
selector: 'cut-date-input',
Expand Down Expand Up @@ -71,17 +72,32 @@ export class DateInputComponent implements ControlValueAccessor, Validator, OnIn
public writeValue(obj: string): void { // 2018-04-09T08:02:27.542
if (obj) {
this.rawValue = this.removeMilliseconds(obj);
// needs to handle also partial dates, e.g. -05-2016 (missing day)
const [datePart, timePart] = this.rawValue.split('T');
const dateValues = datePart.split('-');
this.year = this.displayYear = dateValues[0] || '';
this.month = this.displayMonth = dateValues[1] || '';
this.day = this.displayDay = dateValues[2] || '';
if (timePart) {
const timeParts = timePart.split(':');
this.hour = this.displayHour = timeParts[0] || '';
this.minute = this.displayMinute = timeParts[1] || '';
this.second = this.displaySecond = timeParts[2] || '';

// for DateTime fields, convert from UTC to local time for display
if (this.isDateTime && this.rawValue.includes('T')) {
const utcMoment = moment.utc(this.rawValue);
const localMoment = utcMoment.local();

this.year = this.displayYear = localMoment.format('YYYY');
this.month = this.displayMonth = localMoment.format('MM');
this.day = this.displayDay = localMoment.format('DD');
this.hour = this.displayHour = localMoment.format('HH');
this.minute = this.displayMinute = localMoment.format('mm');
this.second = this.displaySecond = localMoment.format('ss');
} else {
// for Date fields (no time), parse normally
// needs to handle also partial dates, e.g. -05-2016 (missing day)
const [datePart, timePart] = this.rawValue.split('T');
const dateValues = datePart.split('-');
this.year = this.displayYear = dateValues[0] || '';
this.month = this.displayMonth = dateValues[1] || '';
this.day = this.displayDay = dateValues[2] || '';
if (timePart) {
const timeParts = timePart.split(':');
this.hour = this.displayHour = timeParts[0] || '';
this.minute = this.displayMinute = timeParts[1] || '';
this.second = this.displaySecond = timeParts[2] || '';
}
}
}
}
Expand Down Expand Up @@ -227,7 +243,14 @@ export class DateInputComponent implements ControlValueAccessor, Validator, OnIn
this.minute ? this.pad(this.minute) : '',
this.second ? this.pad(this.second) : ''
].join(':');
return `${date}T${time}.000`;
const localDateTimeString = `${date}T${time}.000`;

// convert from local time to UTC for storage
const localMoment = moment(localDateTimeString);
const utcMoment = localMoment.utc();

// return in the expected format
return utcMoment.format('YYYY-MM-DDTHH:mm:ss.000');
} else {
return date;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angul
import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import * as moment from 'moment';
import { CaseEditDataService } from '../../../commons/case-edit-data/case-edit-data.service';
import { CaseField, Jurisdiction } from '../../../domain/definition';
import { MockRpxTranslatePipe } from '../../../test/mock-rpx-translate.pipe';
Expand Down Expand Up @@ -619,8 +620,8 @@ describe('ReadCaseFlagFieldComponent', () => {
expect(component.flagsData[2].flags.roleOnCase).toBeUndefined();
expect(component.flagsData[2].flags.details.length).toBe(1);
expect(component.flagsData[2].flags.details[0].name).toEqual(caseLevelFlagDetailsValue.name);
expect(component.flagsData[2].flags.details[0].dateTimeModified).toEqual(new Date(caseLevelFlagDetailsValue.dateTimeModified));
expect(component.flagsData[2].flags.details[0].dateTimeCreated).toEqual(new Date(caseLevelFlagDetailsValue.dateTimeCreated));
expect(component.flagsData[2].flags.details[0].dateTimeModified).toEqual(moment.utc(caseLevelFlagDetailsValue.dateTimeModified).local().toDate());
expect(component.flagsData[2].flags.details[0].dateTimeCreated).toEqual(moment.utc(caseLevelFlagDetailsValue.dateTimeCreated).local().toDate());
expect(component.flagsData[2].flags.details[0].hearingRelevant).toBe(true);
expect(component.flagsData[3].flags.flagsCaseFieldId).toEqual(witnessCaseFlagGroupInternalFieldId);
expect(component.flagsData[3].flags.partyName).toEqual(witnessCaseFlagPartyName);
Expand Down Expand Up @@ -702,8 +703,8 @@ describe('ReadCaseFlagFieldComponent', () => {
expect(component.flagsData[2].flags.roleOnCase).toBeUndefined();
expect(component.flagsData[2].flags.details.length).toBe(1);
expect(component.flagsData[2].flags.details[0].name).toEqual(caseLevelFlagDetailsValue.name);
expect(component.flagsData[2].flags.details[0].dateTimeModified).toEqual(new Date(caseLevelFlagDetailsValue.dateTimeModified));
expect(component.flagsData[2].flags.details[0].dateTimeCreated).toEqual(new Date(caseLevelFlagDetailsValue.dateTimeCreated));
expect(component.flagsData[2].flags.details[0].dateTimeModified).toEqual(moment.utc(caseLevelFlagDetailsValue.dateTimeModified).local().toDate());
expect(component.flagsData[2].flags.details[0].dateTimeCreated).toEqual(moment.utc(caseLevelFlagDetailsValue.dateTimeCreated).local().toDate());
expect(component.flagsData[2].flags.details[0].hearingRelevant).toBe(true);
expect(component.flagsData[3].flags.flagsCaseFieldId).toEqual(witnessCaseFlagGroupInternalFieldId);
expect(component.flagsData[3].flags.partyName).toEqual(witnessCaseFlagPartyName);
Expand Down Expand Up @@ -779,8 +780,8 @@ describe('ReadCaseFlagFieldComponent', () => {
expect(component.flagsData[2].flags.roleOnCase).toBeUndefined();
expect(component.flagsData[2].flags.details.length).toBe(1);
expect(component.flagsData[2].flags.details[0].name).toEqual(caseLevelFlagDetailsValue.name);
expect(component.flagsData[2].flags.details[0].dateTimeModified).toEqual(new Date(caseLevelFlagDetailsValue.dateTimeModified));
expect(component.flagsData[2].flags.details[0].dateTimeCreated).toEqual(new Date(caseLevelFlagDetailsValue.dateTimeCreated));
expect(component.flagsData[2].flags.details[0].dateTimeModified).toEqual(moment.utc(caseLevelFlagDetailsValue.dateTimeModified).local().toDate());
expect(component.flagsData[2].flags.details[0].dateTimeCreated).toEqual(moment.utc(caseLevelFlagDetailsValue.dateTimeCreated).local().toDate());
expect(component.flagsData[2].flags.details[0].hearingRelevant).toBe(true);
expect(component.flagsData[3].flags.flagsCaseFieldId).toEqual(witnessCaseFlagGroupInternalFieldId);
expect(component.flagsData[3].flags.partyName).toEqual(witnessCaseFlagPartyName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { AbstractFieldReadComponent } from '../base-field/abstract-field-read.component';

@Component({
selector: 'ccd-read-date-field',
template: `<span class="text-16">{{caseField.value | ccdDate:'utc':caseField.dateTimeDisplayFormat}}</span>`
template: `<span class="text-16">{{caseField.value | ccdDate:timeZone:caseField.dateTimeDisplayFormat}}</span>`
})
export class ReadDateFieldComponent extends AbstractFieldReadComponent {
export class ReadDateFieldComponent extends AbstractFieldReadComponent implements OnInit{
public timeZone = 'utc';

public ngOnInit(): void {
super.ngOnInit();
if (this.caseField?.field_type.id === 'DateTime') {
this.timeZone = 'local';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
<div class="govuk-form-group bottom-30" [id]="caseField.id"
[ngClass]="{'form-group-error': dateControl && !dateControl.valid && dateControl.dirty}">
[ngClass]="{'form-group-error': localDisplayControl && !localDisplayControl.valid && localDisplayControl.dirty}">
<fieldset>
<legend>
<span class="form-label" *ngIf="caseField.label">{{(caseField | ccdFieldLabel)}}</span>
<span class="form-hint" *ngIf="caseField.hint_text">{{caseField.hint_text | rpxTranslate}}</span>
<span class="error-message"
*ngIf="dateControl && dateControl.errors && dateControl.dirty && !(minError || maxError)">{{(dateControl.errors | ccdFirstError:caseField.label)}}</span>
*ngIf="localDisplayControl && localDisplayControl.errors && localDisplayControl.dirty && !(minError || maxError)">{{(localDisplayControl.errors | ccdFirstError:caseField.label)}}</span>
<span class="error-message"
*ngIf="dateControl && dateControl.dirty && minError">{{'This date is older than the minimum date allowed' | rpxTranslate}}</span>
*ngIf="localDisplayControl && localDisplayControl.dirty && minError">{{'This date is older than the minimum date allowed' | rpxTranslate}}</span>
<span class="error-message"
*ngIf="dateControl && dateControl.dirty && maxError">{{'This date is later than the maximum date allowed' | rpxTranslate}}</span>
*ngIf="localDisplayControl && localDisplayControl.dirty && maxError">{{'This date is later than the maximum date allowed' | rpxTranslate}}</span>
</legend>
<div class="datepicker-container">
<input class="govuk-input"
#input
attr.aria-label="Please enter a date and time in the format | rpxTranslate {{dateTimeEntryFormat}}"
[min]="minDate(caseField)"
[max]="maxDate(caseField)"
[formControl]="dateControl"
[formControl]="localDisplayControl"
[ngxMatDatetimePicker]="picker"
(focusin)="focusIn()"
(focusout)="focusOut()"
(dateChange)="valueChanged()"
ng-model-options="{timezone:'utc'}"
>
<mat-datepicker-toggle matSuffix [for]="picker" id="pickerOpener"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #picker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { CUSTOM_MOMENT_FORMATS } from './datetime-picker-utils';
useClass: NgxMatMomentAdapter,
deps: [MAT_LEGACY_DATE_LOCALE, NGX_MAT_MOMENT_DATE_ADAPTER_OPTIONS]
},
{ provide: NGX_MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } }
{ provide: NGX_MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: false } }
]
})

Expand Down Expand Up @@ -58,6 +58,8 @@ export class DatetimePickerComponent extends AbstractFormFieldComponent implemen

@Input() public dateControl: FormControl = new FormControl(new Date());

public localDisplayControl: FormControl;

private minimumDate = new Date('01/01/1800');
private maximumDate = null;
private momentFormat = 'YYYY-MM-DDTHH:mm:ss.SSS';
Expand All @@ -70,21 +72,54 @@ export class DatetimePickerComponent extends AbstractFormFieldComponent implemen
public ngOnInit(): void {
this.dateTimeEntryFormat = this.formatTranslationService.showOnlyDates(this.caseField.dateTimeEntryFormat);
this.configureDatePicker(this.dateTimeEntryFormat);
// set date control based on mandatory field

const existingControl = (this.parent || this.formGroup)?.controls?.[this.caseField.id];

// for when navigating back to an existing form
this.dateControl = (this.caseField.isMandatory ?
this.registerControl(new FormControl(this.caseField.value || '', [Validators.required]))
: this.registerControl(new FormControl(this.caseField.value))) as FormControl;

let initialUtcValue = this.dateControl.value;
let initialLocalValue: string;

// for DateTime fields, convert UTC to local for display
if (initialUtcValue && this.caseField.field_type.type === 'DateTime') {
const utcMoment = moment.utc(initialUtcValue);
const localMoment = utcMoment.local();
initialLocalValue = localMoment.format('YYYY-MM-DDTHH:mm:ss.SSS');
} else {
initialLocalValue = initialUtcValue || '';
}

this.localDisplayControl = new FormControl(initialLocalValue);

// sync local display control to main control with UTC conversion
this.localDisplayControl.valueChanges.subscribe(localValue => {
if (this.caseField.field_type.type === 'DateTime' && localValue) {
const parsedLocal = moment(localValue, this.momentFormat);
if (parsedLocal.isValid()) {
const utcValue = parsedLocal.utc().format(this.momentFormat);
this.dateControl.setValue(utcValue, { emitEvent: false });
} else {
this.dateControl.setValue(localValue, { emitEvent: false });
}
} else {
this.dateControl.setValue(localValue, { emitEvent: false });
}
});

this.localDisplayControl.statusChanges.subscribe(() => {
this.minError = this.localDisplayControl.hasError('matDatetimePickerMin');
this.maxError = this.localDisplayControl.hasError('matDatetimePickerMax');
});

// in resetting the format just after the page initialises, the input can be reformatted
// otherwise the last format given will be how the text shown will be displayed
setTimeout(() => {
this.setDateTimeFormat();
this.formatValueAndSetErrors();
}, 1000);
// when the status changes check that the maximum/minimum date has not been exceeded
this.dateControl.statusChanges.subscribe(() => {
this.minError = this.dateControl.hasError('matDatetimePickerMin');
this.maxError = this.dateControl.hasError('matDatetimePickerMax');
});
}

public setDateTimeFormat(): void {
Expand Down Expand Up @@ -182,16 +217,16 @@ export class DatetimePickerComponent extends AbstractFormFieldComponent implemen

public yearSelected(event: Moment): void {
if (this.startView === 'multi-year' && this.yearSelection) {
this.dateControl.patchValue(event.toISOString());
this.localDisplayControl.patchValue(event.toISOString());
this.datetimePicker.close();
this.valueChanged();
}
}

public monthSelected(event: Moment): void {
if (this.startView === 'multi-year') {
this.dateControl.patchValue(event.toISOString());
this.dateControl.patchValue(event.toISOString());
this.localDisplayControl.patchValue(event.toISOString());
this.localDisplayControl.patchValue(event.toISOString());
this.datetimePicker.close();
this.valueChanged();
}
Expand All @@ -200,19 +235,35 @@ export class DatetimePickerComponent extends AbstractFormFieldComponent implemen
private formatValueAndSetErrors(): void {
if (this.inputElement.nativeElement.value) {
let formValue = this.inputElement.nativeElement.value;
formValue = moment(formValue, this.dateTimeEntryFormat).format(this.momentFormat);
if (formValue !== 'Invalid date') {
// if not invalid set the value as the formatted value
this.dateControl.setValue(formValue);
const parsedMoment = moment(formValue, this.dateTimeEntryFormat);

if (parsedMoment.isValid()) {
// format the value in local time
// localDisplayControl will auto-sync to dateControl with UTC conversion
formValue = parsedMoment.format(this.momentFormat);
this.localDisplayControl.setValue(formValue);
} else {
// ensure that the datepicker picks up the invalid error
const keepErrorText = this.inputElement.nativeElement.value;
this.localDisplayControl.setValue(keepErrorText);
this.dateControl.setValue(keepErrorText);
this.inputElement.nativeElement.value = keepErrorText;
}
} else {
// ensure required errors are picked up if relevant
this.dateControl.setValue('');
// input is empty - check if we need to sync from control values
if (this.localDisplayControl.value) {
// control has a value but input doesn't - this happens when navigating back
// manually sync the control value to the input element
const controlValue = this.localDisplayControl.value;
const parsedMoment = moment(controlValue, this.momentFormat);
if (parsedMoment.isValid()) {
const formattedValue = parsedMoment.format(this.dateTimeEntryFormat);
this.inputElement.nativeElement.value = formattedValue;
}
} else if (!this.dateControl.value) {
this.localDisplayControl.setValue('');
this.dateControl.setValue('');
}
}
}
}
Loading