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]]);
+ });
+ });
});