diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 19dd96786d..387b6fee80 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,9 @@ ## RELEASE NOTES +### Version 7.0.66 +**EXUI-2148** Additional checks on task completion from session storage +**EXUI-2057** A frozen screen is seen when attempting to complete a current task... + ### Version 7.0.65 **EXUI-2320** Intermittent missing call of search for completable diff --git a/package.json b/package.json index 4297d2fa8a..cdf5356c3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.0.65", + "version": "7.0.66", "engines": { "node": ">=18.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index 12f19937c6..222880d484 100644 --- a/projects/ccd-case-ui-toolkit/package.json +++ b/projects/ccd-case-ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.0.65", + "version": "7.0.66", "engines": { "node": ">=18.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts index 8dfd44ade2..b7d838a7ad 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts @@ -21,6 +21,7 @@ import { ValidPageListCaseFieldsService } from '../services/valid-page-list-case import { CaseEditComponent } from './case-edit.component'; import { AbstractAppConfig } from '../../../../app.config'; import createSpyObj = jasmine.createSpyObj; +import { EventDetails, Task } from '../../../domain/work-allocation/Task'; describe('CaseEditComponent', () => { const EVENT_TRIGGER: CaseEventTrigger = createCaseEventTrigger( @@ -1167,11 +1168,14 @@ describe('CaseEditComponent', () => { describe('submitForm', () => { it('should submit case', () => { + const userInfo = {id: "id"}; + const mockTaskEventCompletionInfo = {taskId: '123', eventId: 'testEvent', caseId: '123456789', userId: '1', createdTimestamp: Date.now()}; + mockSessionStorageService.getItem.and.returnValues(JSON.stringify(CLIENT_CONTEXT), JSON.stringify(mockTaskEventCompletionInfo), JSON.stringify({userInfo})) const mockClass = { submit: () => of({}) }; spyOn(mockClass, 'submit').and.returnValue(of({ - id: 'id', + userInfo: {id: 'id'}, /* tslint:disable:object-literal-key-quotes */ 'callback_response_status': 'CALLBACK_HASNOT_COMPLETED', /* tslint:disable:object-literal-key-quotes */ @@ -1238,8 +1242,8 @@ describe('CaseEditComponent', () => { expect(validPageListCaseFieldsService.validPageListCaseFields).toHaveBeenCalled(); expect(formValueService.removeUnnecessaryFields).toHaveBeenCalled(); // check that tasks removed from session storage once event has been completed - expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith('taskToComplete'); - expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith('taskEvent'); + expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith('clientContext'); + expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith('taskEventCompletionInfo'); }); it('should submit the case for a Case Flags submission', () => { @@ -1471,46 +1475,64 @@ describe('CaseEditComponent', () => { }); describe('taskExistsForThisEventAndCase', () => { - const mockEventId = 'testEvent'; - const mockCaseId = '123456789'; - const mockTaskEvent = {taskId: '123', eventId: 'testEvent'}; + const mockEventDetails: EventDetails = { eventId: 'testEvent', caseId: '123456789', userId: '1' }; it('should return false when there is no task present', () => { - expect(component.taskExistsForThisEventAndCase(null, null, mockEventId, mockCaseId)).toBe(false); + expect(component.taskExistsForThisEvent(null, null, mockEventDetails)).toBe(false); }); it('should return false when there is a task present that does not match the current case', () => { const mockTask = {id: '123', case_id: '987654321'}; - expect(component.taskExistsForThisEventAndCase(mockTask, null, mockEventId, mockCaseId)).toBe(false); + expect(component.taskExistsForThisEvent(mockTask as Task, null, mockEventDetails)).toBe(false); }); it('should return true when there is a task present that matches the current case when there is no event in session storage', () => { const mockTask = {id: '123', case_id: '123456789'}; - expect(component.taskExistsForThisEventAndCase(mockTask, null, mockEventId, mockCaseId)).toBe(true); + expect(component.taskExistsForThisEvent(mockTask as Task, null, mockEventDetails)).toBe(true); }); it('should return true when there is a task present that matches the current case and current event', () => { const mockTask = {id: '123', case_id: '123456789'}; - const mockTaskEvent = {taskId: '123', eventId: 'testEvent'}; - expect(component.taskExistsForThisEventAndCase(mockTask, mockTaskEvent, mockEventId, mockCaseId)).toBe(true); + const mockTaskEventCompletionInfo = {taskId: '123', eventId: 'testEvent', caseId: '123456789', userId: '1', createdTimestamp: Date.now()}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(true); }); it('should return false when there is a task present that matches the current case but does not match the event', () => { const mockTask = {id: '123', case_id: '123456789'}; - const mockTaskEvent = {taskId: '123', eventId: 'testEvent2'}; - expect(component.taskExistsForThisEventAndCase(mockTask, mockTaskEvent, mockEventId, mockCaseId)).toBe(false); + const mockTaskEventCompletionInfo = {taskId: '123', eventId: 'testEvent2', caseId: '123456789', userId: '1', createdTimestamp: Date.now()}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(false); }); it('should return true when there is a task present that matches the current case, does not match the event but does not match the task associated with the event in session storage', () => { // highly unlikely to occur but feasible scenario const mockTask = {id: '123', case_id: '123456789'}; - const mockTaskEvent = {taskId: '1234', eventId: 'testEvent2'}; - expect(component.taskExistsForThisEventAndCase(mockTask, mockTaskEvent, mockEventId, mockCaseId)).toBe(true); + const mockTaskEventCompletionInfo = {taskId: '1234', eventId: 'testEvent2', caseId: '123456789', userId: '1', createdTimestamp: Date.now()}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(true); }); it('should return true when there is a task present that matches the current case, matches the event and does not match the task associated with the event in session storage', () => { const mockTask = {id: '123', case_id: '123456789'}; - const mockTaskEvent = {taskId: '123', eventId: 'testEvent'}; - expect(component.taskExistsForThisEventAndCase(mockTask, mockTaskEvent, mockEventId, mockCaseId)).toBe(true); + const mockTaskEventCompletionInfo = {taskId: '1234', eventId: 'testEvent', caseId: '123456789', userId: '1', createdTimestamp: Date.now()}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(true); + }); + + it('should return false when there is a task present that matches the current case, matches the event but does not match the user', () => { + const mockTask = {id: '123', case_id: '123456789'}; + const mockTaskEventCompletionInfo = {taskId: '123', eventId: 'testEvent', caseId: '123456789', userId: '2', createdTimestamp: Date.now()}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(false); + }); + + it('should return false when there is a task present that matches the current case, matches the event but the timestamp is older than day ago', () => { + const mockTask = {id: '123', case_id: '123456789'}; + const dayAndTwoHoursAgo = new Date().getTime() - (26*60*60*1000); + const mockTaskEventCompletionInfo = {taskId: '123', eventId: 'testEvent', caseId: '123456789', userId: '1', createdTimestamp: dayAndTwoHoursAgo}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(false); + }); + + it('should return true when there is a task present that matches the current case, matches the event but the timestamp is less than day ago', () => { + const mockTask = {id: '123', case_id: '123456789'}; + const twoHoursAgo = new Date().getTime() - (2*60*60*1000); + const mockTaskEventCompletionInfo = {taskId: '123', eventId: 'testEvent', caseId: '123456789', userId: '1', createdTimestamp: twoHoursAgo}; + expect(component.taskExistsForThisEvent(mockTask as Task, mockTaskEventCompletionInfo, mockEventDetails)).toBe(true); }); }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts index cc9c7de768..343d17f576 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; import { Observable, Subject, of } from 'rxjs'; import { finalize, switchMap } from 'rxjs/operators'; +import { AbstractAppConfig } from '../../../../app.config'; import { Constants } from '../../../commons/constants'; import { ConditionalShowRegistrarService, GreyBarService } from '../../../directives'; import { @@ -13,7 +14,8 @@ import { CaseEventData, CaseEventTrigger, CaseField, CaseView, Draft, HttpError, Profile } from '../../../domain'; -import { Task, TaskEvent } from '../../../domain/work-allocation/Task'; +import { UserInfo } from '../../../domain/user/user-info.model'; +import { EventDetails, Task, TaskEventCompletionInfo } from '../../../domain/work-allocation/Task'; import { AlertService, FieldsPurger, FieldsUtils, FormErrorService, FormValueService, LoadingService, @@ -23,7 +25,6 @@ import { Confirmation, Wizard, WizardPage } from '../domain'; import { EventCompletionParams } from '../domain/event-completion-params.model'; import { CaseNotifier, WizardFactoryService, WorkAllocationService } from '../services'; import { ValidPageListCaseFieldsService } from '../services/valid-page-list-caseFields.service'; -import { AbstractAppConfig } from '../../../../app.config'; @Component({ selector: 'ccd-case-edit', @@ -244,15 +245,22 @@ export class CaseEditComponent implements OnInit, OnDestroy { const clientContextStr = this.sessionStorageService.getItem('clientContext'); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); const taskInSessionStorage = userTask ? userTask.task_data : null; - let taskEventInSessionStorage: TaskEvent; - const taskStr = this.sessionStorageService.getItem('taskToComplete'); - const taskEventStr = this.sessionStorageService.getItem('taskEvent'); - if (taskEventStr) { - taskEventInSessionStorage = JSON.parse(taskEventStr); + let taskEventCompletionInfo: TaskEventCompletionInfo; + let userInfo: UserInfo; + const taskEventCompletionStr = this.sessionStorageService.getItem('taskEventCompletionInfo'); + const userInfoStr = this.sessionStorageService.getItem('userDetails'); + const assignNeeded = this.sessionStorageService.getItem('assignNeeded'); + if (taskEventCompletionStr) { + taskEventCompletionInfo = JSON.parse(taskEventCompletionStr); + } + if (userInfoStr) { + userInfo = JSON.parse(userInfoStr); } const eventId = this.getEventId(form); const caseId = this.getCaseId(caseDetails); - if (this.taskExistsForThisEventAndCase(taskInSessionStorage, taskEventInSessionStorage, eventId, caseId)) { + const userId = userInfo.id ? userInfo.id : userInfo.uid; + const eventDetails: EventDetails = {eventId, caseId, userId, assignNeeded}; + if (this.taskExistsForThisEvent(taskInSessionStorage, taskEventCompletionInfo, eventDetails)) { this.abstractConfig.logMessage(`task exist for this event for caseId and eventId as ${caseId} ${eventId}`); // Show event completion component to perform event completion checks this.eventCompletionParams = ({ @@ -260,9 +268,15 @@ export class CaseEditComponent implements OnInit, OnDestroy { eventId, task: taskInSessionStorage }); - // add taskEvent to link current event with task id - const taskEvent = {eventId, taskId: taskInSessionStorage.id}; - this.sessionStorageService.setItem('taskEvent', JSON.stringify(taskEvent)); + // add taskEventCompletionInfo again to ensure link current event with task id + // note: previous usage was created here so this is to ensure correct functionality continues + const taskEventCompletionInfo: TaskEventCompletionInfo = { + caseId, + eventId, + userId, + taskId: taskInSessionStorage.id, + createdTimestamp: Date.now()}; + this.sessionStorageService.setItem('taskEventCompletionInfo', JSON.stringify(taskEventCompletionInfo)); this.isEventCompletionChecksRequired = true; } else { // Task not in session storage, proceed to submit @@ -441,9 +455,9 @@ export class CaseEditComponent implements OnInit, OnDestroy { return this.postCompleteTaskIfRequired(); }),finalize(() => { this.loadingService.unregister(loadingSpinnerToken); - // on event completion ensure the previous event taskToComplete/taskEvent removed - this.sessionStorageService.removeItem('taskToComplete'); - this.sessionStorageService.removeItem('taskEvent') + // on event completion ensure the previous event clientContext/taskEventCompletionInfo removed + this.sessionStorageService.removeItem('clientContext'); + this.sessionStorageService.removeItem('taskEventCompletionInfo') this.isSubmitting = false; })) .subscribe( @@ -512,21 +526,31 @@ export class CaseEditComponent implements OnInit, OnDestroy { } } - // checks whether current taskToComplete relevant for the event - public taskExistsForThisEventAndCase(taskInSessionStorage, taskEvent, eventId, caseId): boolean { - if (!taskInSessionStorage || taskInSessionStorage.case_id !== caseId) { + // checks whether current clientContext relevant for the event + public taskExistsForThisEvent(taskInSessionStorage: Task, taskEventCompletionInfo: TaskEventCompletionInfo, eventDetails: EventDetails): boolean { + if (!taskInSessionStorage || taskInSessionStorage.case_id !== eventDetails.caseId) { return false; } - if (!taskEvent) { + if (!taskEventCompletionInfo) { // if no task event present then there is no task to complete from previous event present return true; } else { - if (taskEvent.taskId === taskInSessionStorage.id && taskEvent.eventId !== eventId) { + if (taskEventCompletionInfo.taskId !== taskInSessionStorage.id) { + return true; + } else if ((taskEventCompletionInfo.taskId === taskInSessionStorage.id && + this.eventDetailsDoNotMatch(taskEventCompletionInfo, eventDetails)) + || this.eventMoreThanDayAgo(taskEventCompletionInfo.createdTimestamp) + ) { // if the session storage not related to event, ignore it and remove - this.sessionStorageService.removeItem('taskToComplete'); - this.sessionStorageService.removeItem('taskEvent'); + this.sessionStorageService.removeItem('clientContext'); + this.sessionStorageService.removeItem('taskEventCompletionInfo'); return false; } + if (eventDetails.assignNeeded === 'false' && eventDetails.userId !== taskInSessionStorage.assignee) { + // if the user does not match task assignee, assign is now needed + // data cannot be deleted and ignored as it matches understanding + this.sessionStorageService.setItem('assignNeeded', 'true'); + } return true; } } @@ -549,4 +573,20 @@ export class CaseEditComponent implements OnInit, OnDestroy { private hasCallbackFailed(response: object): boolean { return response['callback_response_status'] !== 'CALLBACK_COMPLETED'; } + + private eventMoreThanDayAgo(timestamp: number) { + if ((new Date().getTime() - timestamp) > (24*60*60*1000)) { + return true; + } + return false; + } + + private eventDetailsDoNotMatch(taskEventCompletionInfo: TaskEventCompletionInfo, eventDetails: EventDetails) { + if (taskEventCompletionInfo.eventId !== eventDetails.eventId + || taskEventCompletionInfo.caseId !== eventDetails.caseId + || taskEventCompletionInfo.userId !== eventDetails.userId) { + return true; + } + return false; + } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-editor.module.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-editor.module.ts index b78b6a2e99..26f5716cf1 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-editor.module.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-editor.module.ts @@ -51,6 +51,7 @@ import { CaseEditWizardGuard } from './services/case-edit-wizard.guard'; import { CaseFlagStateService } from './services/case-flag-state.service'; import { CaseworkerService } from './services/case-worker.service'; import { ValidPageListCaseFieldsService } from './services/valid-page-list-caseFields.service'; +import { CaseEventCompletionTaskReassignedComponent } from './case-event-completion'; @NgModule({ imports: [ diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.spec.ts index 27c27f978c..39ffc77f61 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.spec.ts @@ -1,9 +1,7 @@ import { PortalModule } from '@angular/cdk/portal'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, EventEmitter, SimpleChange } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { CaseEventCompletionTaskCancelledComponent, CaseEventCompletionTaskReassignedComponent } from '.'; import { AbstractAppConfig } from '../../../../app.config'; @@ -11,18 +9,16 @@ import { Task } from '../../../domain/work-allocation/Task'; import { AlertService, HttpErrorService, HttpService } from '../../../services'; import { SessionStorageService } from '../../../services/session/session-storage.service'; import { EventCompletionParams } from '../domain/event-completion-params.model'; -import { EventCompletionPortalTypes } from '../domain/event-completion-portal-types.model'; import { CaseworkerService, JudicialworkerService } from '../services'; import { EventCompletionStateMachineService } from '../services/event-completion-state-machine.service'; import { WorkAllocationService } from '../services/work-allocation.service'; -import { CaseEventCompletionComponent, COMPONENT_PORTAL_INJECTION_TOKEN } from './case-event-completion.component'; +import { CaseEventCompletionComponent } from './case-event-completion.component'; import createSpyObj = jasmine.createSpyObj; describe('CaseEventCompletionComponent', () => { const API_URL = 'http://aggregated.ccd.reform'; let fixture: ComponentFixture; let component: CaseEventCompletionComponent; - let de: DebugElement; let appConfig: any; let httpService: HttpService; let errorService: HttpErrorService; @@ -32,11 +28,6 @@ describe('CaseEventCompletionComponent', () => { let mockCaseworkerService: CaseworkerService; let mockJudicialworkerService: JudicialworkerService; let eventCompletionStateMachineService: any; - let parentComponent: any; - // tslint:disable-next-line: prefer-const - let mockRouter: Router; - // tslint:disable-next-line: prefer-const - let mockRoute: ActivatedRoute; const task: Task = { assignee: null, @@ -85,25 +76,6 @@ describe('CaseEventCompletionComponent', () => { mockJudicialworkerService = new JudicialworkerService(httpService, appConfig, errorService); eventCompletionStateMachineService = createSpyObj('EventCompletionStateMachineService', ['initialiseStateMachine', 'createStates', 'addTransitions', 'startStateMachine']); - const context = { - task, - caseId: '1620409659381330', - eventId: null, - reassignedTask: null, - router: mockRouter, - route: mockRoute, - sessionStorageService: null, - workAllocationService: mockWorkAllocationService, - alertService, - canBeCompleted: false, - component: this - }; - - parentComponent = { - context, - eventCanBeCompleted: new EventEmitter(true) - }; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -123,8 +95,7 @@ describe('CaseEventCompletionComponent', () => { { provide: AlertService, useValue: alertService }, { provide: EventCompletionStateMachineService, useValue: eventCompletionStateMachineService }, { provide: CaseworkerService, useValue: mockCaseworkerService }, - { provide: JudicialworkerService, useValue: mockJudicialworkerService }, - { provide: COMPONENT_PORTAL_INJECTION_TOKEN, useValue: parentComponent } + { provide: JudicialworkerService, useValue: mockJudicialworkerService } ], }) .compileComponents(); @@ -132,7 +103,6 @@ describe('CaseEventCompletionComponent', () => { fixture = TestBed.createComponent(CaseEventCompletionComponent); component = fixture.componentInstance; component.eventCompletionParams = eventCompletionParams; - de = fixture.debugElement; fixture.detectChanges(); })); @@ -155,11 +125,15 @@ describe('CaseEventCompletionComponent', () => { expect(eventCompletionStateMachineService.startStateMachine).toHaveBeenCalled(); }); - it('should load task cancelled component in cdk portal', () => { - component.context = context; - component.showPortal(EventCompletionPortalTypes.TaskCancelled); - const heading: DebugElement = fixture.debugElement.query(By.css('.govuk-heading-m')); - expect(component.selectedComponentPortal).toBeTruthy(); + it('should emit false if there is now no task to complete in session storage', () => { + spyOn(component.eventCanBeCompleted, 'emit'); + component.setEventCanBeCompleted(false); + expect(component.eventCanBeCompleted.emit).toHaveBeenCalledWith(false); + }); + + it('should set task state', () => { + component.setTaskState(1); + expect(component.taskState).toBe(1); }); afterAll(() => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.ts index 1161304ed8..b61d8a619e 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.component.ts @@ -1,11 +1,10 @@ -import { ComponentPortal } from '@angular/cdk/portal'; -import { Component, EventEmitter, InjectionToken, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, ComponentRef, EventEmitter, InjectionToken, Injector, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { StateMachine } from '@edium/fsm'; import { AlertService } from '../../../services/alert/alert.service'; import { SessionStorageService } from '../../../services/session/session-storage.service'; import { EventCompletionParams } from '../domain/event-completion-params.model'; -import { EventCompletionPortalTypes } from '../domain/event-completion-portal-types.model'; +import { EventCompletionTaskStates } from '../domain/event-completion-task-states.model'; import { EventCompletionComponentEmitter, EventCompletionStateMachineContext } from '../domain/event-completion-state-machine-context.model'; import { EventCompletionStateMachineService } from '../services/event-completion-state-machine.service'; import { WorkAllocationService } from '../services/work-allocation.service'; @@ -25,9 +24,11 @@ export class CaseEventCompletionComponent implements OnChanges, EventCompletionC @Output() public eventCanBeCompleted: EventEmitter = new EventEmitter(); + eventCompletionTaskStates = EventCompletionTaskStates; + public stateMachine: StateMachine; public context: EventCompletionStateMachineContext; - public selectedComponentPortal: ComponentPortal; + public taskState: EventCompletionTaskStates; constructor(private readonly service: EventCompletionStateMachineService, private readonly router: Router, @@ -38,7 +39,7 @@ export class CaseEventCompletionComponent implements OnChanges, EventCompletionC } public ngOnChanges(changes?: SimpleChanges): void { - if (changes.eventCompletionParams && changes.eventCompletionParams.currentValue) { + if (changes.eventCompletionParams?.currentValue) { // Setup the context this.context = { task: this.eventCompletionParams.task, @@ -64,20 +65,16 @@ export class CaseEventCompletionComponent implements OnChanges, EventCompletionC } } - public showPortal(portalType: number): void { - const injector = Injector.create({ - providers: [ - {provide: COMPONENT_PORTAL_INJECTION_TOKEN, useValue: this} - ] - }); - // tslint:disable-next-line:switch-default - switch (portalType) { - case EventCompletionPortalTypes.TaskCancelled: - this.selectedComponentPortal = new ComponentPortal(CaseEventCompletionTaskCancelledComponent, null, injector); - break; - case EventCompletionPortalTypes.TaskReassigned: - this.selectedComponentPortal = new ComponentPortal(CaseEventCompletionTaskReassignedComponent, null, injector); - break; + public setTaskState(taskState: number): void { + this.taskState = taskState; + } + + public setEventCanBeCompleted(completable: boolean) { + // note: event not completed from here as will then skip task completion + if (!completable) { + // if event cannot be completed ensure that this is communicated + // otherwise this will be handled via onchanges + this.eventCanBeCompleted.emit(completable); } } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.html b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.html index 679cf0c961..f8ce2ff57f 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.html +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/case-event-completion.html @@ -1 +1,10 @@ - + + + + \ No newline at end of file diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts index 68abde6f85..c2c6a76cae 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts @@ -1,55 +1,53 @@ -import { DebugElement, EventEmitter } from '@angular/core'; +import { Component, Input, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { MockRpxTranslatePipe } from '../../../../../test/mock-rpx-translate.pipe'; -import { COMPONENT_PORTAL_INJECTION_TOKEN } from '../../case-event-completion.component'; +import { EventCompletionStateMachineContext } from '../../../domain'; import { CaseEventCompletionTaskCancelledComponent } from './case-event-completion-task-cancelled.component'; +@Component({ + template: '' +}) +class WrapperComponent { + @ViewChild(CaseEventCompletionTaskCancelledComponent, { static: true }) public appComponentRef: CaseEventCompletionTaskCancelledComponent; + @Input() public context: EventCompletionStateMachineContext; +} + describe('TaskCancelledComponent', () => { let component: CaseEventCompletionTaskCancelledComponent; - let mockParentComponent: any; - let fixture: ComponentFixture; - - mockParentComponent = { - context: { - task: { - assignee: '1234-1234-1234-1234' - }, - caseId: '1620409659381330' - }, - eventCanBeCompleted: new EventEmitter(true) - }; + let wrapper: WrapperComponent; + let fixture: ComponentFixture; beforeEach(async () => { TestBed.configureTestingModule({ imports: [ RouterTestingModule, ], - declarations: [CaseEventCompletionTaskCancelledComponent, MockRpxTranslatePipe], - providers: [ - {provide: COMPONENT_PORTAL_INJECTION_TOKEN, useValue: mockParentComponent} - ] + declarations: [CaseEventCompletionTaskCancelledComponent, MockRpxTranslatePipe, WrapperComponent], }) .compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(CaseEventCompletionTaskCancelledComponent); - component = fixture.componentInstance; + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + component = fixture.componentInstance.appComponentRef; + const sessionStorageSpy = jasmine.createSpyObj('mockSessionStorageService', ['removeItem']); + wrapper.context = {caseId: '123456789', sessionStorageService: sessionStorageSpy} as EventCompletionStateMachineContext; fixture.detectChanges(); }); it('should display error message task cancelled', () => { - const heading: DebugElement = fixture.debugElement.query(By.css('.govuk-heading-m')); - const headingHtml = heading.nativeElement as HTMLElement; - expect(headingHtml.innerText).toBe('Task cancelled/marked as done'); + const element = fixture.debugElement.nativeElement; + const heading = element.querySelector('.govuk-heading-m'); + expect(heading.textContent).toBe('Task cancelled/marked as done'); }); it('should emit event can be completed true when clicked on continue button', () => { - spyOn(mockParentComponent.eventCanBeCompleted, 'emit'); + spyOn(component.notifyEventCompletionCancelled, 'emit'); component.onContinue(); - expect(mockParentComponent.eventCanBeCompleted.emit).toHaveBeenCalledWith(true); + expect(component.context.sessionStorageService.removeItem).toHaveBeenCalledWith('clientContext') + expect(component.notifyEventCompletionCancelled.emit).toHaveBeenCalledWith(true); }); afterAll(() => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts index 1550ecffda..c2f5840dbb 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts @@ -1,19 +1,27 @@ -import { Component, Inject } from '@angular/core'; +import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { COMPONENT_PORTAL_INJECTION_TOKEN, CaseEventCompletionComponent } from '../../case-event-completion.component'; +import { EventCompletionStateMachineContext } from '../../../domain'; @Component({ selector: 'app-case-event-completion-task-cancelled', templateUrl: './case-event-completion-task-cancelled.html' }) -export class CaseEventCompletionTaskCancelledComponent { +export class CaseEventCompletionTaskCancelledComponent implements OnInit { + @Input() + context: EventCompletionStateMachineContext; + @Output() + public notifyEventCompletionCancelled: EventEmitter = new EventEmitter(); + public caseId: string; - constructor(@Inject(COMPONENT_PORTAL_INJECTION_TOKEN) private readonly parentComponent: CaseEventCompletionComponent) { - this.caseId = this.parentComponent.context.caseId; + public ngOnInit(): void { + this.caseId = this.context.caseId; } public onContinue(): void { - // Emit event can be completed event - this.parentComponent.eventCanBeCompleted.emit(true); + // Removes task to complete so event completes without task + this.context.sessionStorageService.removeItem('clientContext'); + // may be able to remove this call below since it is now unneccesary + this.notifyEventCompletionCancelled.emit(true); } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.spec.ts index 4caf21c03c..dff4ea0190 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.spec.ts @@ -1,4 +1,4 @@ -import { DebugElement, EventEmitter } from '@angular/core'; +import { Component, DebugElement, Input, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; @@ -10,15 +10,24 @@ import { Judicialworker } from '../../../../../domain/work-allocation/judicial-w import { Task } from '../../../../../domain/work-allocation/Task'; import { AlertService, HttpErrorService, HttpService, SessionStorageService } from '../../../../../services'; import { MockRpxTranslatePipe } from '../../../../../test/mock-rpx-translate.pipe'; +import { EventCompletionStateMachineContext } from '../../../domain'; import { CaseworkerService, JudicialworkerService, WorkAllocationService } from '../../../services'; -import { COMPONENT_PORTAL_INJECTION_TOKEN } from '../../case-event-completion.component'; import { CaseEventCompletionTaskReassignedComponent } from './case-event-completion-task-reassigned.component'; import createSpyObj = jasmine.createSpyObj; +@Component({ + template: '' +}) +class WrapperComponent { + @ViewChild(CaseEventCompletionTaskReassignedComponent, { static: true }) public appComponentRef: CaseEventCompletionTaskReassignedComponent; + @Input() public context: EventCompletionStateMachineContext; +} + describe('TaskReassignedComponent', () => { const API_URL = 'http://aggregated.ccd.reform'; let component: CaseEventCompletionTaskReassignedComponent; - let fixture: ComponentFixture; + let wrapper: WrapperComponent; + let fixture: ComponentFixture; let mockSessionStorageService: any; let appConfig: any; let httpService: HttpService; @@ -27,7 +36,6 @@ describe('TaskReassignedComponent', () => { let mockCaseworkerService: CaseworkerService; let mockJudicialworkerService: JudicialworkerService; let mockWorkAllocationService: WorkAllocationService; - let parentComponent: any; const task: Task = { assignee: '1234-1234-1234-1234', @@ -105,37 +113,29 @@ describe('TaskReassignedComponent', () => { mockCaseworkerService = new CaseworkerService(httpService, appConfig, errorService); mockJudicialworkerService = new JudicialworkerService(httpService, appConfig, errorService); - parentComponent = { - context: { - reassignedTask: { - assignee: '1234-1234-1234-1234' - } - }, - eventCanBeCompleted: new EventEmitter(true) - }; - beforeEach(async () => { mockSessionStorageService = createSpyObj('sessionStorageService', ['getItem', 'setItem']); mockSessionStorageService.getItem.and.returnValue(JSON.stringify(CLIENT_CONTEXT)); TestBed.configureTestingModule({ imports: [RouterTestingModule], - declarations: [CaseEventCompletionTaskReassignedComponent, MockRpxTranslatePipe], + declarations: [CaseEventCompletionTaskReassignedComponent, MockRpxTranslatePipe, WrapperComponent], providers: [ {provide: ActivatedRoute, useValue: mockRoute}, {provide: AlertService, useValue: alertService}, {provide: SessionStorageService, useValue: mockSessionStorageService}, {provide: WorkAllocationService, useValue: mockWorkAllocationService}, {provide: CaseworkerService, useValue: mockCaseworkerService}, - {provide: JudicialworkerService, useValue: mockJudicialworkerService}, - {provide: COMPONENT_PORTAL_INJECTION_TOKEN, useValue: parentComponent} + {provide: JudicialworkerService, useValue: mockJudicialworkerService} ] }) .compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(CaseEventCompletionTaskReassignedComponent); - component = fixture.componentInstance; + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + component = fixture.componentInstance.appComponentRef; + wrapper.context = {caseId: '1620409659381330', reassignedTask: task} as EventCompletionStateMachineContext; spyOn(mockCaseworkerService, 'getCaseworkers').and.returnValue(of(null)); spyOn(mockJudicialworkerService, 'getJudicialworkers').and.returnValue(of([judicialworker])); fixture.detectChanges(); @@ -154,7 +154,7 @@ describe('TaskReassignedComponent', () => { spyOn(mockWorkAllocationService, 'assignAndCompleteTask').and.returnValue({subscribe: () => {}}); component.onContinue(); expect(mockSessionStorageService.getItem).toHaveBeenCalledTimes(1); - expect(mockSessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'true'); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'true - override'); }); it('should task on continue event', () => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts index 0c3036513e..1b55aafa0e 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts @@ -1,22 +1,23 @@ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Subscription, throwError } from 'rxjs'; -import { Task } from '../../../../../domain/work-allocation/Task'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Subscription } from 'rxjs'; import { - AlertService, FieldsUtils, SessionStorageService } from '../../../../../services'; +import { EventCompletionStateMachineContext } from '../../../domain'; import { CaseworkerService } from '../../../services/case-worker.service'; import { JudicialworkerService } from '../../../services/judicial-worker.service'; -import { WorkAllocationService } from '../../../services/work-allocation.service'; -import { COMPONENT_PORTAL_INJECTION_TOKEN, CaseEventCompletionComponent } from '../../case-event-completion.component'; @Component({ selector: 'app-case-event-completion-task-reassigned', templateUrl: './case-event-completion-task-reassigned.html' }) export class CaseEventCompletionTaskReassignedComponent implements OnInit, OnDestroy { + @Input() + context: EventCompletionStateMachineContext; + @Output() + public notifyEventCompletionReassigned: EventEmitter = new EventEmitter(); + public caseId: string; public assignedUserId: string; public assignedUserName: string; @@ -24,19 +25,15 @@ export class CaseEventCompletionTaskReassignedComponent implements OnInit, OnDes public caseworkerSubscription: Subscription; public judicialworkerSubscription: Subscription; - constructor(@Inject(COMPONENT_PORTAL_INJECTION_TOKEN) private readonly parentComponent: CaseEventCompletionComponent, - private readonly route: ActivatedRoute, - private readonly workAllocationService: WorkAllocationService, - private readonly sessionStorageService: SessionStorageService, + constructor(private readonly sessionStorageService: SessionStorageService, private readonly judicialworkerService: JudicialworkerService, - private readonly caseworkerService: CaseworkerService, - private readonly alertService: AlertService) { + private readonly caseworkerService: CaseworkerService) { } public ngOnInit(): void { // Get case id and task from the parent component - this.caseId = this.parentComponent.context.caseId; - const task = this.parentComponent.context.reassignedTask; + this.caseId = this.context.caseId; + const task = this.context.reassignedTask; // Current user is a caseworker? this.caseworkerSubscription = this.caseworkerService.getCaseworkers(task.jurisdiction).subscribe(result => { @@ -86,12 +83,12 @@ export class CaseEventCompletionTaskReassignedComponent implements OnInit, OnDes // not complete_task not utilised here as related to event completion // service wanting task associated with event to not be completed not directly relevant if (task) { - this.sessionStorageService.setItem('assignNeeded', 'true'); - // set event can be completed to true - this.parentComponent.eventCanBeCompleted.emit(true); + // Set session to override reassignment settings so code flow does not return to this component + this.sessionStorageService.setItem('assignNeeded', 'true - override') + this.notifyEventCompletionReassigned.emit(true); } else { // Emit event cannot be completed event - this.parentComponent.eventCanBeCompleted.emit(false); + this.notifyEventCompletionReassigned.emit(false); } } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-portal-types.model.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-portal-types.model.ts deleted file mode 100644 index f0a4a27993..0000000000 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-portal-types.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum EventCompletionPortalTypes { - TaskCancelled, - TaskReassigned -} diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-state-machine-context.model.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-state-machine-context.model.ts index 448658f408..6cf5773518 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-state-machine-context.model.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-state-machine-context.model.ts @@ -7,7 +7,7 @@ import { WorkAllocationService } from '../services/work-allocation.service'; export interface EventCompletionComponentEmitter { eventCanBeCompleted: EventEmitter; - showPortal(portalType: number); + setTaskState(taskState: number); } export interface EventCompletionStateMachineContext { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-task-states.model.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-task-states.model.ts new file mode 100644 index 0000000000..c18c815f7f --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/domain/event-completion-task-states.model.ts @@ -0,0 +1,4 @@ +export enum EventCompletionTaskStates { + TaskCancelled, + TaskReassigned +} diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.spec.ts index ec8bc358fc..4caffa5bb9 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.spec.ts @@ -7,7 +7,7 @@ import { of } from 'rxjs'; import { WorkAllocationService } from '.'; import { AbstractAppConfig } from '../../../../app.config'; import { Task } from '../../../domain/work-allocation/Task'; -import { TaskRespone } from '../../../domain/work-allocation/task-response.model'; +import { TaskResponse } from '../../../domain/work-allocation/task-response.model'; import { AlertService, HttpErrorService, HttpService, SessionStorageService } from '../../../services'; import { EventCompletionStateMachineContext, @@ -15,13 +15,14 @@ import { } from '../domain'; import { EventCompletionStateMachineService } from './event-completion-state-machine.service'; import createSpyObj = jasmine.createSpyObj; +import { EventCompletionTaskStates } from '../domain/event-completion-task-states.model'; describe('EventCompletionStateMachineService', () => { const API_URL = 'http://aggregated.ccd.reform'; let service: EventCompletionStateMachineService; let stateMachine: StateMachine; // tslint:disable-next-line: prefer-const - let mockSessionStorageService: jasmine.SpyObj; + let mockSessionStorageService: SessionStorageService; let appConfig: jasmine.SpyObj; let httpService: HttpService; let errorService: HttpErrorService; @@ -31,7 +32,8 @@ describe('EventCompletionStateMachineService', () => { let mockRoute: ActivatedRoute; let mockRouter: any; const eventCompletionComponentEmitter: any = { - eventCanBeCompleted: new EventEmitter(true) + eventCanBeCompleted: new EventEmitter(true), + setTaskState: () => { } }; mockRouter = { @@ -39,16 +41,40 @@ describe('EventCompletionStateMachineService', () => { routerState: {} }; - const CLIENT_CONTEXT = { client_context: { - user_task: { - task_data: { - id: '1', - name: 'Example task', - case_id: '1234567890' - }, - complete_task: true + const CLIENT_CONTEXT = { + client_context: { + user_task: { + task_data: { + assignee: '1234-1234-1234-1234', + auto_assigned: false, + case_category: 'asylum', + case_id: '1620409659381330', + case_management_category: null, + case_name: 'Alan Jonson', + case_type_id: null, + created_date: '2021-04-19T14:00:00.000+0000', + due_date: '2021-05-20T16:00:00.000+0000', + execution_type: null, + id: '0d22d838-b25a-11eb-a18c-f2d58a9b7bc6', + jurisdiction: 'Immigration and Asylum', + location: null, + location_name: null, + name: 'Task name', + permissions: null, + region: null, + security_classification: null, + task_state: 'assigned', + task_system: null, + task_title: 'Some lovely task name', + type: null, + warning_list: null, + warnings: true, + work_type_id: null + }, + complete_task: true + } } - }}; + }; const oneTask: Task = { assignee: '1234-1234-1234-1234', @@ -85,9 +111,8 @@ describe('EventCompletionStateMachineService', () => { httpService = createSpyObj('httpService', ['get', 'post']); errorService = createSpyObj('errorService', ['setError']); alertService = createSpyObj('alertService', ['clear', 'warning', 'setPreserveAlerts']); - mockSessionStorageService = createSpyObj('sessionStorageService', ['getItem', 'setItem']); - mockSessionStorageService.getItem.and.returnValue(JSON.stringify(CLIENT_CONTEXT)); mockWorkAllocationService = new WorkAllocationService(httpService, appConfig, errorService, alertService, mockSessionStorageService); + mockSessionStorageService = new SessionStorageService(); const context: EventCompletionStateMachineContext = { task: oneTask, @@ -107,8 +132,9 @@ describe('EventCompletionStateMachineService', () => { TestBed.configureTestingModule({ imports: [RouterTestingModule], providers: [ - {provide: Router, useValue: mockRouter}, - {provide: WorkAllocationService, useValue: mockWorkAllocationService} + { provide: Router, useValue: mockRouter }, + { provide: WorkAllocationService, useValue: mockWorkAllocationService }, + { provide: SessionStorageService, useValue: mockSessionStorageService } ] }); service = new EventCompletionStateMachineService(); @@ -147,69 +173,95 @@ describe('EventCompletionStateMachineService', () => { }); it('should perform state task assigned to user', () => { - const taskResponse: TaskRespone = { - task: oneTask - }; - spyOn(context.workAllocationService, 'getTask').and.returnValue(of({taskResponse})); + const taskResponse = { task: oneTask }; + spyOn(context.workAllocationService, 'getTask').and.returnValue(of(taskResponse)); oneTask.task_state = 'assigned'; oneTask.assignee = '1234-1234-1234-1234'; context.task = oneTask; + CLIENT_CONTEXT.client_context.user_task.task_data = oneTask as any; + spyOn(context.sessionStorageService, 'getItem').and.returnValues('false', JSON.stringify(CLIENT_CONTEXT)); + spyOn(context.sessionStorageService, 'setItem'); stateMachine = service.initialiseStateMachine(context); service.createStates(stateMachine); service.addTransitions(); service.startStateMachine(stateMachine); - expect(stateMachine.currentState.id).toEqual(EventCompletionStates.CheckTasksCanBeCompleted); + expect(stateMachine.currentState.id).toEqual(EventCompletionStates.Final); expect(context.workAllocationService.getTask).toHaveBeenCalled(); + expect(context.sessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'false'); }); it('should perform state task assigned to another user', () => { - const task = oneTask; - const taskResponse: TaskRespone = { - task: oneTask - }; - spyOn(context.workAllocationService, 'getTask').and.returnValue(of({taskResponse})); + const task = { ...oneTask }; + const taskResponse = { task: oneTask }; + spyOn(context.workAllocationService, 'getTask').and.returnValue(of(taskResponse)); task.task_state = 'assigned'; task.assignee = '4321-4321-4321-4321'; context.task = task; + CLIENT_CONTEXT.client_context.user_task.task_data = task as any; + spyOn(context.sessionStorageService, 'getItem').and.returnValues('false', JSON.stringify(CLIENT_CONTEXT)); + spyOn(context.component, 'setTaskState'); stateMachine = service.initialiseStateMachine(context); service.createStates(stateMachine); service.addTransitions(); service.startStateMachine(stateMachine); - expect(stateMachine.currentState.id).toEqual(EventCompletionStates.CheckTasksCanBeCompleted); + expect(stateMachine.currentState.id).toEqual(EventCompletionStates.Final); expect(context.workAllocationService.getTask).toHaveBeenCalled(); + expect(context.component.setTaskState).toHaveBeenCalledWith(EventCompletionTaskStates.TaskReassigned); + }); + + it('should perform state task assigned to another user with override', () => { + const task = { ...oneTask }; + const taskResponse = { task: oneTask }; + spyOn(context.workAllocationService, 'getTask').and.returnValue(of(taskResponse)); + task.task_state = 'assigned'; + task.assignee = '4321-4321-4321-4321'; + context.task = task; + CLIENT_CONTEXT.client_context.user_task.task_data = task as any; + spyOn(context.sessionStorageService, 'getItem').and.returnValues('true - override', JSON.stringify(CLIENT_CONTEXT)); + spyOn(context.sessionStorageService, 'setItem'); + stateMachine = service.initialiseStateMachine(context); + service.createStates(stateMachine); + service.addTransitions(); + service.startStateMachine(stateMachine); + expect(stateMachine.currentState.id).toEqual(EventCompletionStates.Final); + expect(context.workAllocationService.getTask).toHaveBeenCalled(); + expect(context.sessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'true'); }); it('should perform state task unassigned', () => { const taskToTest = oneTask; taskToTest.assignee = null; taskToTest.task_state = 'unassigned'; - const taskResponse: TaskRespone = { - task: taskToTest - }; - spyOn(context.workAllocationService, 'getTask').and.returnValue(of({taskResponse})); + const taskResponse = { task: taskToTest }; + CLIENT_CONTEXT.client_context.user_task.task_data = taskToTest as any; + spyOn(context.workAllocationService, 'getTask').and.returnValue(of(taskResponse)); + spyOn(context.sessionStorageService, 'getItem').and.returnValues('false', JSON.stringify(CLIENT_CONTEXT)); + spyOn(context.sessionStorageService, 'setItem'); context.task = taskToTest; stateMachine = service.initialiseStateMachine(context); service.createStates(stateMachine); service.addTransitions(); service.startStateMachine(stateMachine); - expect(stateMachine.currentState.id).toEqual(EventCompletionStates.CheckTasksCanBeCompleted); + expect(stateMachine.currentState.id).toEqual(EventCompletionStates.Final); expect(context.workAllocationService.getTask).toHaveBeenCalled(); + expect(context.sessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'true'); }); it('should perform state task completed or cancelled', () => { const taskToTest = oneTask; taskToTest.task_state = 'completed'; - const taskResponse: TaskRespone = { - task: taskToTest - }; - spyOn(context.workAllocationService, 'getTask').and.returnValue(of({taskResponse})); + const taskResponse = { task: taskToTest }; + spyOn(context.workAllocationService, 'getTask').and.returnValue(of(taskResponse)); + spyOn(context.sessionStorageService, 'getItem').and.returnValue('false'); + spyOn(context.component, 'setTaskState'); context.task = taskToTest; stateMachine = service.initialiseStateMachine(context); service.createStates(stateMachine); service.addTransitions(); service.startStateMachine(stateMachine); - expect(stateMachine.currentState.id).toEqual(EventCompletionStates.CheckTasksCanBeCompleted); + expect(stateMachine.currentState.id).toEqual(EventCompletionStates.Final); expect(context.workAllocationService.getTask).toHaveBeenCalled(); + expect(context.component.setTaskState).toHaveBeenCalledWith(EventCompletionTaskStates.TaskCancelled); }); it('should add transition for state check taks can be completed', () => { @@ -247,22 +299,6 @@ describe('EventCompletionStateMachineService', () => { expect(service.addTransitionsForStateTaskUnassigned).toBeTruthy(); }); - it('should correctly set assignNeeded when checking task', () => { - stateMachine = service.initialiseStateMachine(context); - service.createStates(stateMachine); - const state = createSpyObj('state', ['trigger']); - service.entryActionForStateCompleteEventAndTask(state, context); - expect(mockSessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'false'); - }); - - it('should correctly set assignNeeded when checking unassigned task', () => { - stateMachine = service.initialiseStateMachine(context); - service.createStates(stateMachine); - const state = createSpyObj('state', ['trigger']); - service.entryActionForStateTaskUnassigned(state, context); - expect(mockSessionStorageService.setItem).toHaveBeenCalledWith('assignNeeded', 'true'); - }); - afterAll(() => { TestBed.resetTestingModule(); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts index 348d6259af..3271052dab 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; import { State, StateMachine } from '@edium/fsm'; import { throwError } from 'rxjs'; -import { Task, TaskState } from '../../../domain/work-allocation/Task'; -import { EventCompletionPortalTypes } from '../domain/event-completion-portal-types.model'; +import { TaskState } from '../../../domain/work-allocation/Task'; +import { FieldsUtils } from '../../../services'; import { EventCompletionStateMachineContext } from '../domain/event-completion-state-machine-context.model'; import { EventCompletionStates } from '../domain/event-completion-states.enum.model'; -import { FieldsUtils } from '../../../services'; +import { EventCompletionTaskStates } from '../domain/event-completion-task-states.model'; const EVENT_COMPLETION_STATE_MACHINE = 'EVENT COMPLETION STATE MACHINE'; @@ -80,6 +80,7 @@ export class EventCompletionStateMachineService { } public entryActionForStateCheckTasksCanBeCompleted(state: State, context: EventCompletionStateMachineContext): void { + const assignNeeded = context.sessionStorageService.getItem('assignNeeded'); context.workAllocationService.getTask(context.task.id).subscribe( taskResponse => { if (taskResponse && taskResponse.task && taskResponse.task.task_state) { @@ -99,6 +100,10 @@ export class EventCompletionStateMachineService { if (taskResponse.task.assignee === context.task.assignee) { // Task still assigned to current user, complete event and task state.trigger(EventCompletionStates.CompleteEventAndTask); + } else if (assignNeeded === 'true - override') { + // this will treat task as unassigned instead of reassigned to complete after user confirmation + // assignNeeded will also be immediately overwritten to true + state.trigger(EventCompletionStates.TaskUnassigned); } else { // Task has been reassigned to another user, display error message context.reassignedTask = taskResponse.task; @@ -122,7 +127,7 @@ export class EventCompletionStateMachineService { // Trigger final state to complete processing of state machine state.trigger(EventCompletionStates.Final); // Load case event completion task cancelled component - context.component.showPortal(EventCompletionPortalTypes.TaskCancelled); + context.component.setTaskState(EventCompletionTaskStates.TaskCancelled); } public entryActionForStateCompleteEventAndTask(state: State, context: EventCompletionStateMachineContext): void { @@ -144,7 +149,7 @@ export class EventCompletionStateMachineService { // Trigger final state to complete processing of state machine state.trigger(EventCompletionStates.Final); // Load case event completion task reassigned component - context.component.showPortal(EventCompletionPortalTypes.TaskReassigned); + context.component.setTaskState(EventCompletionTaskStates.TaskReassigned); } public entryActionForStateTaskUnassigned(state: State, context: EventCompletionStateMachineContext): void { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.spec.ts index ae4ada120f..f4c20e6304 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.spec.ts @@ -2,7 +2,7 @@ import { waitForAsync } from '@angular/core/testing'; import { of, throwError } from 'rxjs'; import { AbstractAppConfig } from '../../../../app.config'; import { HttpError, TaskSearchParameter } from '../../../domain'; -import { TaskRespone } from '../../../domain/work-allocation/task-response.model'; +import { TaskResponse } from '../../../domain/work-allocation/task-response.model'; import { HttpErrorService, HttpService } from '../../../services'; import { MULTIPLE_TASKS_FOUND, WorkAllocationService } from './work-allocation.service'; @@ -64,7 +64,7 @@ function getExampleUserDetails(): UserDetails[] { }]; } -function getExampleTask(): TaskRespone { +function getExampleTask(): TaskResponse { return { task: { assignee: '1234-1234-1234-1234', diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts index 4abe9447a9..0339548ce1 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts @@ -4,7 +4,7 @@ import { catchError, map } from 'rxjs/operators'; import { AbstractAppConfig } from '../../../../app.config'; import { TaskSearchParameter, WAFeatureConfig } from '../../../domain'; import { UserDetails } from '../../../domain/user/user-details.model'; -import { TaskRespone } from '../../../domain/work-allocation/task-response.model'; +import { TaskResponse } from '../../../domain/work-allocation/task-response.model'; import { TaskPayload } from '../../../domain/work-allocation/TaskPayload'; import { AlertService, HttpErrorService, HttpService, SessionStorageService } from '../../../services'; @@ -236,7 +236,7 @@ export class WorkAllocationService { /** * Call the API to get a task */ - public getTask(taskId: string): Observable { + public getTask(taskId: string): Observable { if (!this.isWAEnabled()) { return of({task: null}); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts index 397cac9f4e..693e3b2642 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts @@ -183,7 +183,7 @@ describe('EventStartGuard', () => { it('should return true and store task if taskId is provided and task is found in payload', () => { const payload: TaskPayload = { task_required_for_event: false, tasks: tasks }; sessionStorageService.getItem.and.returnValue(JSON.stringify({ cid: 'caseId' })); - const result$ = guard['checkForTasks'](payload, 'caseId', 'eventId', 'taskId'); + const result$ = guard['checkForTasks'](payload, 'caseId', 'eventId', 'taskId', 'userId'); result$.subscribe(result => { expect(result).toBe(true); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts index 03cd5f437d..81feae8093 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts @@ -2,10 +2,12 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; + +import { AbstractAppConfig } from '../../../../app.config'; +import { TaskEventCompletionInfo } from '../../../domain/work-allocation/Task'; import { TaskPayload } from '../../../domain/work-allocation/TaskPayload'; import { SessionStorageService } from '../../../services'; import { WorkAllocationService } from '../../case-editor'; -import { AbstractAppConfig } from '../../../../app.config'; @Injectable() export class EventStartGuard implements CanActivate { @@ -21,15 +23,20 @@ export class EventStartGuard implements CanActivate { const caseId = route.params['cid']; const eventId = route.params['eid']; const taskId = route.queryParams['tid']; - + let userId: string; + const userInfoStr = this.sessionStorageService.getItem('userDetails'); + if (userInfoStr) { + const userInfo = JSON.parse(userInfoStr); + userId = userInfo.id ? userInfo.id : userInfo.uid; + } const caseInfoStr = this.sessionStorageService.getItem('caseInfo'); if (caseInfoStr) { const caseInfo = JSON.parse(caseInfoStr); if (caseInfo && caseInfo.cid === caseId) { return this.workAllocationService.getTasksByCaseIdAndEventId(eventId, caseId, caseInfo.caseType, caseInfo.jurisdiction) .pipe( - switchMap((payload: TaskPayload) => this.checkForTasks(payload, caseId, eventId, taskId)) - ); + switchMap((payload: TaskPayload) => this.checkForTasks(payload, caseId, eventId, taskId, userId)) + ); } else { this.abstractConfig.logMessage(`EventStartGuard: caseId ${caseInfo.cid} in caseInfo not matched with the route parameter caseId ${caseId}`); } @@ -86,11 +93,28 @@ export class EventStartGuard implements CanActivate { this.sessionStorageService.removeItem(EventStartGuard.CLIENT_CONTEXT); } - private checkForTasks(payload: TaskPayload, caseId: string, eventId: string, taskId: string): Observable { + private checkForTasks(payload: TaskPayload, caseId: string, eventId: string, taskId: string, userId: string): Observable { if (taskId && payload?.tasks?.length > 0) { const task = payload.tasks.find((t) => t.id == taskId); if (task) { - this.sessionStorageService.setItem(EventStartGuard.CLIENT_CONTEXT, JSON.stringify(task)); + // Store task to session + const taskEventCompletionInfo: TaskEventCompletionInfo = { + caseId: caseId, + eventId: eventId, + userId: userId, + taskId: task.id, + createdTimestamp: Date.now() + }; + const storeClientContext = { + client_context: { + user_task: { + task_data: task, + complete_task: true + } + } + }; + this.sessionStorageService.setItem('taskEventCompletionInfo', JSON.stringify(taskEventCompletionInfo)); + this.sessionStorageService.setItem(EventStartGuard.CLIENT_CONTEXT, JSON.stringify(storeClientContext)); } else { this.removeTaskFromSessionStorage(); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts index 6a401dd91f..8ab746f4b4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { State, StateMachine } from '@edium/fsm'; import { EventStartStateMachineContext, EventStartStates } from '../models'; +import { TaskEventCompletionInfo } from '../../../domain/work-allocation/Task'; +import { UserInfo } from '../../../domain/user/user-info.model'; const EVENT_STATE_MACHINE = 'EVENT STATE MACHINE'; @@ -194,6 +196,19 @@ export class EventStartStateMachineService { } }; context.sessionStorageService.setItem('clientContext', JSON.stringify(clientContext)); + let userInfo: UserInfo; + const userInfoStr = context.sessionStorageService.getItem('userDetails'); + if (userInfoStr) { + userInfo = JSON.parse(userInfoStr); + } + // Store task to session + const taskEventCompletionInfo: TaskEventCompletionInfo = { + caseId: context.caseId, + eventId: context.eventId, + userId: userInfo.id ? userInfo.id : userInfo.uid, + taskId: task.id, + createdTimestamp: Date.now()}; + context.sessionStorageService.setItem('taskEventCompletionInfo', JSON.stringify(taskEventCompletionInfo)); // Allow user to perform the event context.router.navigate([`/cases/case-details/${context.caseId}/trigger/${context.eventId}`], { relativeTo: context.route }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts index 2688bd8016..be72e165aa 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts @@ -31,9 +31,20 @@ export interface UserTask { complete_task: boolean } -export interface TaskEvent { +// Query: UserID will be removed on logout/login (is it necessary)? +export interface TaskEventCompletionInfo { taskId: string; eventId: string; + caseId: string; + userId: string; + createdTimestamp: number; +} + +export interface EventDetails { + eventId: string; + caseId: string; + userId: string; + assignNeeded?: string; } export enum Permissions { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/task-response.model.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/task-response.model.ts index d542d42654..f2f5f07a90 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/task-response.model.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/task-response.model.ts @@ -1,5 +1,5 @@ import { Task } from './Task'; -export interface TaskRespone { +export interface TaskResponse { task: Task; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/window/window.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/window/window.service.spec.ts index e0df06a223..332fd76825 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/window/window.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/window/window.service.spec.ts @@ -3,7 +3,6 @@ import { WindowService } from './window.service'; describe('WindowService', () => { const windowService: WindowService = new WindowService(); const userName = 'test user'; - const organisationDetails = 'test organisation'; it('should remove from local storage', () => { windowService.setLocalStorage('user', userName);