diff --git a/tests/page-objects/tasks/tasks.wdio.page.js b/tests/page-objects/tasks/tasks.wdio.page.js index 723e6d99b88..5e4b8d96316 100644 --- a/tests/page-objects/tasks/tasks.wdio.page.js +++ b/tests/page-objects/tasks/tasks.wdio.page.js @@ -6,7 +6,7 @@ const noSelectedTaskSelector = '.empty-selection'; const tasksList = () => $(taskListSelector); const getTaskById = (emissionId) => $(`${taskListSelector} li[data-record-id="${emissionId}"`); -const getTasks = () => $$(`${taskListSelector} li`); +const getTasks = () => $$(`${taskListSelector} li.content-row`); const getTaskInfo = async (taskElement) => { const contactName = await (await taskElement.$('h4 span')).getText(); diff --git a/webapp/src/ts/modules/tasks/tasks.component.html b/webapp/src/ts/modules/tasks/tasks.component.html index e8d999675d8..2ff5537dc64 100644 --- a/webapp/src/ts/modules/tasks/tasks.component.html +++ b/webapp/src/ts/modules/tasks/tasks.component.html @@ -32,6 +32,7 @@

{{task.priorityLabel}}
+
diff --git a/webapp/src/ts/modules/tasks/tasks.component.ts b/webapp/src/ts/modules/tasks/tasks.component.ts index 2a7946e90df..12479796f60 100644 --- a/webapp/src/ts/modules/tasks/tasks.component.ts +++ b/webapp/src/ts/modules/tasks/tasks.component.ts @@ -13,19 +13,25 @@ import { Selectors } from '@mm-selectors/index'; import { TelemetryService } from '@mm-services/telemetry.service'; import { TourService } from '@mm-services/tour.service'; import { GlobalActions } from '@mm-actions/global'; +import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service'; +import { UserContactService } from '@mm-services/user-contact.service'; +import { SessionService } from '@mm-services/session.service'; @Component({ templateUrl: './tasks.component.html', }) export class TasksComponent implements OnInit, OnDestroy { constructor( - private store:Store, - private changesService:ChangesService, - private contactTypesService:ContactTypesService, - private rulesEngineService:RulesEngineService, - private telemetryService:TelemetryService, - private tourService:TourService, - private route:ActivatedRoute, + private store: Store, + private changesService: ChangesService, + private contactTypesService: ContactTypesService, + private rulesEngineService: RulesEngineService, + private telemetryService: TelemetryService, + private tourService: TourService, + private route: ActivatedRoute, + private lineageModelGeneratorService: LineageModelGeneratorService, + private userContactService: UserContactService, + private sessionService: SessionService, ) { this.tasksActions = new TasksActions(store); this.globalActions = new GlobalActions(store); @@ -41,6 +47,7 @@ export class TasksComponent implements OnInit, OnDestroy { hasTasks; loading; tasksDisabled; + currentLevel; private tasksLoaded; private debouncedReload; @@ -97,6 +104,9 @@ export class TasksComponent implements OnInit, OnDestroy { this.hasTasks = false; this.loading = true; this.debouncedReload = _debounce(this.refreshTasks.bind(this), 1000, { maxWait: 10 * 1000 }); + + this.currentLevel = this.sessionService.isOnlineOnly() ? Promise.resolve() : this.getCurrentLineageLevel(); + this.refreshTasks(); this.tourService.startIfNeeded(this.route.snapshot); @@ -128,40 +138,78 @@ export class TasksComponent implements OnInit, OnDestroy { }); } - private refreshTasks() { - const telemetryData:any = { - start: Date.now(), - }; - - return this.rulesEngineService - .isEnabled() - .then(isEnabled => { - this.tasksDisabled = !isEnabled; - return isEnabled ? this.rulesEngineService.fetchTaskDocsForAllContacts() : []; - }) - .then(taskDocs => { - this.hasTasks = taskDocs.length > 0; - this.loading = false; - this.tasksActions.setTasksList(this.hydrateEmissions(taskDocs)); - if (!this.tasksLoaded) { - this.tasksActions.setTasksLoaded(true); - } - - telemetryData.end = Date.now(); - const telemetryEntryName = !this.tasksLoaded ? `tasks:load`: `tasks:refresh`; - this.telemetryService.record(telemetryEntryName, telemetryData.end - telemetryData.start); - }) - .catch(err => { - console.error('Error getting tasks for all contacts', err); - - this.error = true; - this.loading = false; - this.hasTasks = false; - this.tasksActions.setTasksList([]); - }); + private async refreshTasks() { + try { + const telemetryData: any = { + start: Date.now(), + }; + + const isEnabled = await this.rulesEngineService.isEnabled(); + this.tasksDisabled = !isEnabled; + const taskDocs = isEnabled ? await this.rulesEngineService.fetchTaskDocsForAllContacts() : []; + + this.hasTasks = taskDocs.length > 0; + this.loading = false; + + const hydratedTasks = await this.hydrateEmissions(taskDocs) || []; + const subjects = await this.getLineagesFromTaskDocs(hydratedTasks); + if (subjects?.size) { + const userLineageLevel = await this.currentLevel; + hydratedTasks.forEach(task => { + task.lineage = this.getTaskLineage(subjects, task, userLineageLevel); + }); + } + + this.tasksActions.setTasksList(hydratedTasks); + + if (!this.tasksLoaded) { + this.tasksActions.setTasksLoaded(true); + } + + telemetryData.end = Date.now(); + const telemetryEntryName = !this.tasksLoaded ? `tasks:load` : `tasks:refresh`; + this.telemetryService.record(telemetryEntryName, telemetryData.end - telemetryData.start); + + } catch (exception) { + console.error('Error getting tasks for all contacts', exception); + this.error = true; + this.loading = false; + this.hasTasks = false; + this.tasksActions.setTasksList([]); + } } listTrackBy(index, task) { return task?._id; } + + private getCurrentLineageLevel() { + return this.userContactService.get().then(user => user?.parent?.name); + } + + private getLineagesFromTaskDocs(taskDocs) { + const ids = [...new Set(taskDocs.map(task => task.owner))]; + return this.lineageModelGeneratorService + .reportSubjects(ids) + .then(subjects => new Map(subjects.map(subject => [subject._id, subject.lineage]))); + } + + private getTaskLineage(subjects, task, userLineageLevel) { + const lineage = subjects + .get(task.owner) + ?.map(lineage => lineage?.name); + return this.cleanAndRemoveCurrentLineage(lineage, userLineageLevel); + } + + private cleanAndRemoveCurrentLineage(lineage, userLineageLevel) { + if (!lineage?.length) { + return; + } + lineage = lineage.filter(level => level); + const item = lineage[lineage.length - 1]; + if (item === userLineageLevel) { + lineage.pop(); + } + return lineage; + } } diff --git a/webapp/tests/karma/ts/modules/tasks/tasks.component.spec.ts b/webapp/tests/karma/ts/modules/tasks/tasks.component.spec.ts index 353b9a69bb2..dee17f7f3af 100644 --- a/webapp/tests/karma/ts/modules/tasks/tasks.component.spec.ts +++ b/webapp/tests/karma/ts/modules/tasks/tasks.component.spec.ts @@ -16,6 +16,9 @@ import { TasksComponent } from '@mm-modules/tasks/tasks.component'; import { NavigationComponent } from '@mm-components/navigation/navigation.component'; import { Selectors } from '@mm-selectors/index'; import { NavigationService } from '@mm-services/navigation.service'; +import { UserContactService } from '@mm-services/user-contact.service'; +import { SessionService } from '@mm-services/session.service'; +import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service'; describe('TasksComponent', () => { let getComponent; @@ -26,10 +29,21 @@ describe('TasksComponent', () => { let contactTypesService; let clock; let store; + let sessionService; + let userContactService; + let lineageModelGeneratorService; let component: TasksComponent; let fixture: ComponentFixture; + const userContactDoc = { + _id: 'user', + parent: { + _id: 'parent', + name: 'parent', + }, + }; + beforeEach(async () => { changesService = { subscribe: sinon.stub() }; rulesEngineService = { @@ -43,6 +57,13 @@ describe('TasksComponent', () => { contactTypesService = { includes: sinon.stub(), }; + sessionService = { + isOnlineOnly: sinon.stub().returns(false), + }; + userContactService = { + get: sinon.stub().resolves(), + }; + lineageModelGeneratorService = { reportSubjects: sinon.stub().resolves([]) }; TestBed.configureTestingModule({ imports: [ @@ -57,6 +78,9 @@ describe('TasksComponent', () => { { provide: TourService, useValue: tourService }, { provide: ContactTypesService, useValue: contactTypesService }, { provide: NavigationService, useValue: {} }, + { provide: SessionService, useValue: sessionService }, + { provide: UserContactService, useValue: userContactService }, + { provide: LineageModelGeneratorService, useValue: lineageModelGeneratorService }, ], declarations: [ TasksComponent, @@ -149,17 +173,6 @@ describe('TasksComponent', () => { { _id: '1', emission: { _id: 'e1', dueDate: futureDate.format('YYYY-MM-DD') }, owner: 'a' }, { _id: '2', emission: { _id: 'e2', dueDate: pastDate.format('YYYY-MM-DD') }, owner: 'b' }, ]; - rulesEngineService.fetchTaskDocsForAllContacts.resolves(taskDocs); - - await new Promise(resolve => { - sinon.stub(TasksActions.prototype, 'setTasksList').callsFake(resolve); - getComponent(); - }); - - expect(component.loading).to.be.false; - expect(component.tasksDisabled).to.be.false; - expect(component.hasTasks).to.be.true; - expect(!!component.error).to.be.false; const expectedTasks = [ { _id: 'e1', @@ -176,6 +189,17 @@ describe('TasksComponent', () => { owner: 'b', }, ]; + + rulesEngineService.fetchTaskDocsForAllContacts.resolves(taskDocs); + await new Promise(resolve => { + sinon.stub(TasksActions.prototype, 'setTasksList').callsFake(resolve); + getComponent(); + }); + + expect(component.loading).to.be.false; + expect(component.tasksDisabled).to.be.false; + expect(component.hasTasks).to.be.true; + expect(!!component.error).to.be.false; expect((TasksActions.prototype.setTasksList).args).to.deep.eq([[expectedTasks]]); }); @@ -278,4 +302,139 @@ describe('TasksComponent', () => { expect(component.listTrackBy(0, false)).to.equal(undefined); }); }); + + describe('lineage and breadcrumbs', () => { + const bettysContactDoc = { + _id: 'user', + parent: { + _id: 'parent', + name: 'CHW Bettys Area', + }, + }; + const taskDocs = [ + { _id: '1', emission: { _id: 'e1', dueDate: '2020-10-20' }, forId: 'a', owner: 'a' }, + { _id: '2', emission: { _id: 'e2', dueDate: '2020-10-20' }, forId: 'b', owner: 'b' }, + ]; + const taskLineages = [ + { + _id: 'a', + lineage: [ + { name: 'Amy Johnsons Household' }, + { name: 'St Elmos Concession' }, + { name: 'Chattanooga Village' }, + { name: 'CHW Bettys Area' }, + null, + ], + }, + { + _id: 'b', + lineage: [ + { name: 'Amy Johnsons Household' }, + { name: 'St Elmos Concession' }, + { name: 'Chattanooga Village' }, + null, + null, + ], + }, + ]; + + it('should not alter tasks lineage if user is online only', async () => { + const expectedTasks = [ + { + _id: 'e1', + date: moment('2020-10-20').toDate(), + dueDate: '2020-10-20', + lineage: [ 'Amy Johnsons Household', 'St Elmos Concession', 'Chattanooga Village', 'CHW Bettys Area' ], + overdue: true, + owner: 'a', + }, + { + _id: 'e2', + date: moment('2020-10-20').toDate(), + dueDate: '2020-10-20', + lineage: [ 'Amy Johnsons Household', 'St Elmos Concession', 'Chattanooga Village' ], + overdue: true, + owner: 'b', + }, + ]; + userContactService.get.resolves(bettysContactDoc); + sessionService.isOnlineOnly.returns(true); + rulesEngineService.fetchTaskDocsForAllContacts.resolves(taskDocs); + lineageModelGeneratorService.reportSubjects.resolves(taskLineages); + + await new Promise(resolve => { + sinon.stub(TasksActions.prototype, 'setTasksList').callsFake(resolve); + getComponent(); + }); + + expect(await component.currentLevel).to.be.undefined; + expect((TasksActions.prototype.setTasksList).args).to.deep.equal([[expectedTasks]]); + }); + + it('should not change the tasks lineage if user is offline with unrelated lineage', async () => { + const expectedTasks = [ + { + _id: 'e1', + date: moment('2020-10-20').toDate(), + dueDate: '2020-10-20', + lineage: [ 'Amy Johnsons Household', 'St Elmos Concession', 'Chattanooga Village', 'CHW Bettys Area' ], + overdue: true, + owner: 'a', + }, + { + _id: 'e2', + date: moment('2020-10-20').toDate(), + dueDate: '2020-10-20', + lineage: [ 'Amy Johnsons Household', 'St Elmos Concession', 'Chattanooga Village' ], + overdue: true, + owner: 'b', + }, + ]; + userContactService.get.resolves(userContactDoc); + sessionService.isOnlineOnly.returns(false); + rulesEngineService.fetchTaskDocsForAllContacts.resolves(taskDocs); + lineageModelGeneratorService.reportSubjects.resolves(taskLineages); + + await new Promise(resolve => { + sinon.stub(TasksActions.prototype, 'setTasksList').callsFake(resolve); + getComponent(); + }); + + expect(await component.currentLevel).to.equal('parent'); + expect((TasksActions.prototype.setTasksList).args).to.deep.equal([[expectedTasks]]); + }); + + it('should update the tasks lineage if user is offline with related place to lineage', async () => { + const expectedTasks = [ + { + _id: 'e1', + date: moment('2020-10-20').toDate(), + dueDate: '2020-10-20', + lineage: [ 'Amy Johnsons Household', 'St Elmos Concession', 'Chattanooga Village' ], + overdue: true, + owner: 'a', + }, + { + _id: 'e2', + date: moment('2020-10-20').toDate(), + dueDate: '2020-10-20', + lineage: [ 'Amy Johnsons Household', 'St Elmos Concession', 'Chattanooga Village' ], + overdue: true, + owner: 'b', + }, + ]; + userContactService.get.resolves(bettysContactDoc); + sessionService.isOnlineOnly.returns(false); + rulesEngineService.fetchTaskDocsForAllContacts.resolves(taskDocs); + lineageModelGeneratorService.reportSubjects.resolves(taskLineages); + + await new Promise(resolve => { + sinon.stub(TasksActions.prototype, 'setTasksList').callsFake(resolve); + getComponent(); + }); + + expect(await component.currentLevel).to.equal('CHW Bettys Area'); + expect((TasksActions.prototype.setTasksList).args).to.deep.equal([[expectedTasks]]); + }); + }); });