Skip to content
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d7a6d36
calc tasks for notifications
jonathanbataire May 24, 2025
4a8801f
fix date bug
jonathanbataire May 24, 2025
7975c9c
clean up
jonathanbataire May 25, 2025
e29d488
add dbsync
jonathanbataire May 26, 2025
08181fc
Merge branch 'master' into 9255-notifications
jonathanbataire May 26, 2025
a137242
refine service
jonathanbataire May 27, 2025
f23409a
fresh service :tada:
jonathanbataire May 27, 2025
f37c0ba
bug fix
jonathanbataire May 27, 2025
21d2ed6
offline bug fix
jonathanbataire May 28, 2025
24ad96c
Merge branch 'master' into 9255-notifications
jonathanbataire May 29, 2025
db78a22
clear sonar and lint errors
jonathanbataire May 29, 2025
dcb5540
tests
jonathanbataire May 29, 2025
828d106
test clean up
jonathanbataire May 29, 2025
c768104
:heavy_check_mark: test
jonathanbataire May 29, 2025
34aa546
Merge branch 'master' into 9255-notifications
jonathanbataire May 29, 2025
6a179ac
:tada: tests
jonathanbataire May 30, 2025
0abd99b
Merge branch 'master' into 9255-notifications
jonathanbataire May 30, 2025
e4ed7c1
clean up
jonathanbataire May 30, 2025
317f5fd
refresh service
jonathanbataire Jun 1, 2025
375b2b3
Merge branch 'master' into 9255-notifications
jonathanbataire Jun 4, 2025
8dd47fd
Merge branch 'master' into 9255-notifications
jonathanbataire Jun 5, 2025
3fc32da
address feedback
jonathanbataire Jun 10, 2025
ef69962
Merge branch 'master' into 9255-notifications
jonathanbataire Jun 11, 2025
28e0b7b
Merge branch 'master' into 9255-notifications
jonathanbataire Jun 23, 2025
c174a7a
address feedback
jonathanbataire Jun 23, 2025
5857bad
add warning for missing setting
jonathanbataire Jun 23, 2025
bd07cb6
Merge branch 'master' into 9255-notifications
jonathanbataire Jun 24, 2025
30f944f
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 1, 2025
85ab4c7
fix compile error
jonathanbataire Jul 1, 2025
021a7c6
feedback improvements
jonathanbataire Jul 1, 2025
029cc9f
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 1, 2025
45dfbb8
fix tests
jonathanbataire Jul 1, 2025
4f374ac
:tada:
jonathanbataire Jul 1, 2025
fa8479c
clean up tests
jonathanbataire Jul 1, 2025
82226f6
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 1, 2025
3b5213d
:tada:
jonathanbataire Jul 1, 2025
cafa6f4
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 2, 2025
6226305
Update webapp/tests/karma/ts/services/task-notifications.service.spec.ts
jonathanbataire Jul 15, 2025
133f1d8
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 15, 2025
d217147
clean up
jonathanbataire Jul 15, 2025
4331a8e
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 16, 2025
82d5dd4
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 17, 2025
5724e36
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 19, 2025
d5cc452
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 19, 2025
8798459
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 19, 2025
7d0b09d
Merge branch 'master' into 9255-notifications
jonathanbataire Jul 22, 2025
ca3c951
Merge branch 'master' into 9255-notifications
jonathanbataire Aug 1, 2025
3d95aee
add permission as default
jonathanbataire Aug 7, 2025
40af7bc
Merge branch 'master' into 9255-notifications
jonathanbataire Aug 7, 2025
8227757
Merge branch 'master' into 9255-notifications
jonathanbataire Aug 9, 2025
679d801
Merge branch 'master' into 9255-notifications
jonathanbataire Aug 18, 2025
b6f2969
Merge branch 'master' into 9255-notifications
jonathanbataire Aug 19, 2025
064ce3f
Merge branch 'master' into 9255-notifications
jonathanbataire Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions webapp/src/ts/services/android-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GeolocationService } from '@mm-services/geolocation.service';
import { MRDTService } from '@mm-services/mrdt.service';
import { SessionService } from '@mm-services/session.service';
import { NavigationService } from '@mm-services/navigation.service';
import { TasksNotificationService } from '@mm-services/task-notifications.service';

/**
* An API to provide integration with the medic-android app.
Expand All @@ -24,6 +25,7 @@ export class AndroidApiService {
private sessionService:SessionService,
private zone:NgZone,
private navigationService:NavigationService,
private readonly tasksNotificationService: TasksNotificationService,
) { }

private runInZone(property:string, args:any[]=[]) {
Expand Down Expand Up @@ -168,6 +170,14 @@ export class AndroidApiService {
this.sessionService.logout();
}

/**
* Gets notifcations for tasks
* returns {Promise} A promise that resolves to Notification[].
*/
taskNotifications() {
return this.tasksNotificationService.get();
}

/**
* Handle the response from the MRDT app
* @param response The stringified JSON response from the MRDT app.
Expand Down Expand Up @@ -222,5 +232,6 @@ export class AndroidApiService {
smsStatusUpdate: (...args) => this.runInZone('smsStatusUpdate', args),
locationPermissionRequestResolved: () => this.runInZone('locationPermissionRequestResolve'),
resolveCHTExternalAppResponse: (...args) => this.runInZone('resolveCHTExternalAppResponse', args),
taskNotifications: () => this.runInZone('taskNotifications'),
};
}
61 changes: 35 additions & 26 deletions webapp/src/ts/services/rules-engine.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,27 @@ interface DebounceActive {
active?: boolean;
debounceRef?: any;
performance?: any;
params?:any;
promise?:Promise<any>;
resolve?:any;
params?: any;
promise?: Promise<any>;
resolve?: any;
};
}

interface TaskDoc {
_id: string;
emission?: any;
owner: string
state: string;
authoredOn: number;
}

@Injectable({
providedIn: 'root'
})
export class RulesEngineCoreFactoryService {
constructor(
private dbService: DbService
) {}
) { }

get() {
return RulesEngineCore(this.dbService.get());
Expand All @@ -63,23 +71,23 @@ export class RulesEngineService implements OnDestroy {
private observable = new Subject();

constructor(
private translateService:TranslateService,
private authService:AuthService,
private sessionService:SessionService,
private settingsService:SettingsService,
private telemetryService:TelemetryService,
private performanceService:PerformanceService,
private uhcSettingsService:UHCSettingsService,
private userContactService:UserContactService,
private userSettingsService:UserSettingsService,
private parseProvider:ParseProvider,
private changesService:ChangesService,
private contactTypesService:ContactTypesService,
private translateFromService:TranslateFromService,
private rulesEngineCoreFactoryService:RulesEngineCoreFactoryService,
private calendarIntervalService:CalendarIntervalService,
private ngZone:NgZone,
private chtDatasourceService:CHTDatasourceService
private translateService: TranslateService,
private authService: AuthService,
private sessionService: SessionService,
private settingsService: SettingsService,
private telemetryService: TelemetryService,
private performanceService: PerformanceService,
private uhcSettingsService: UHCSettingsService,
private userContactService: UserContactService,
private userSettingsService: UserSettingsService,
private parseProvider: ParseProvider,
private changesService: ChangesService,
private contactTypesService: ContactTypesService,
private translateFromService: TranslateFromService,
private rulesEngineCoreFactoryService: RulesEngineCoreFactoryService,
private calendarIntervalService: CalendarIntervalService,
private ngZone: NgZone,
private chtDatasourceService: CHTDatasourceService
) {
this.initialized = this.initialize();
this.rulesEngineCore = this.rulesEngineCoreFactoryService.get();
Expand Down Expand Up @@ -142,7 +150,7 @@ export class RulesEngineService implements OnDestroy {
this.debounceActive[this.FRESHNESS_KEY] = {
active: true,
performance: {
name: this.getTelemetryTrackName( 'background-refresh', 'cancel'),
name: this.getTelemetryTrackName('background-refresh', 'cancel'),
track: this.performanceService.track()
},
debounceRef: rulesDebounceRef
Expand All @@ -169,15 +177,16 @@ export class RulesEngineService implements OnDestroy {
debounceInfo.active = false;
}

private waitForDebounce(entity):Promise<any> | undefined {
private waitForDebounce(entity): Promise<any> | undefined {
if (this.debounceActive[entity]?.active && this.debounceActive[entity]?.promise) {
return this.debounceActive[entity].promise;
}

return Promise.resolve();
}

private getRulesEngineContext(settingsDoc, userContactDoc, userSettingsDoc, enableTasks, enableTargets, chtScriptApi){
private getRulesEngineContext(settingsDoc, userContactDoc, userSettingsDoc,
enableTasks, enableTargets, chtScriptApi) {
return {
settingsDoc,
userContactDoc,
Expand Down Expand Up @@ -327,7 +336,7 @@ export class RulesEngineService implements OnDestroy {
return this.rulesEngineCore.updateEmissionsFor(_uniq(contactsWithUpdatedTasks));
}

private hydrateTaskDocs(taskDocs: { _id: string; emission?: any; owner: string }[] = []) {
private hydrateTaskDocs(taskDocs: Array<TaskDoc> = []) {
taskDocs.forEach(taskDoc => {
const { emission } = taskDoc;
if (!emission) {
Expand Down Expand Up @@ -501,7 +510,7 @@ export class RulesEngineService implements OnDestroy {
return this.observable.subscribe(callback);
}

private getTelemetryTrackName(...params:string[]) {
private getTelemetryTrackName(...params: string[]) {
return ['rules-engine', ...params].join(':');
}

Expand Down
103 changes: 103 additions & 0 deletions webapp/src/ts/services/task-notifications.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Injectable } from '@angular/core';
import * as moment from 'moment';
import { orderBy } from 'lodash-es';

import { RulesEngineService } from '@mm-services/rules-engine.service';
import { TranslateService } from '@mm-services/translate.service';
import { DBSyncService } from '@mm-services/db-sync.service';

/*
** avoid overloading app with too many notifications especially at once
** 24 for android >= 10
*/
const MAX_NOTIFICATIONS = 24;
const TODAY_TIMESTAMP = 'cht-today-timestamp';
const LATEST_NOTIFICATION_TIMESTAMP = 'cht-latest-notification-timestamp';

export interface Notification {
_id: string,
authoredOn: number,
state: string,
title: string,
contentText: string,
dueDate: string,
}

@Injectable({
providedIn: 'root'
})
export class TasksNotificationService {

constructor(
private readonly rulesEngineService: RulesEngineService,
private readonly translateService: TranslateService,
private readonly dbSyncService: DBSyncService
) { }

private async fetchNotifications(): Promise<Notification[]> {
try {
const today = moment().format('YYYY-MM-DD');
let latestNotificationTimestamp = this.getLatestNotificationTimestamp();
const isEnabled = await this.rulesEngineService.isEnabled();
const taskDocs = isEnabled ? await this.rulesEngineService.fetchTaskDocsForAllContacts() : [];

let notifications = taskDocs
.filter(task => {
return task.state === 'Ready' && task.emission.dueDate === today &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about tasks that are overdue? I'm curious what this system is really trying to achieve.
If a user doesn't log in for one day, when a specific task is due, and logs in the next day when the task is overdue, they never get notified for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see that's one scenario that wasn't considered
yeah we can add overdue tasks

task.authoredOn > latestNotificationTimestamp;
})
.map(task => ({
_id: task._id,
authoredOn: task.authoredOn,
state: task.state,
title: task.emission.title,
contentText: this.translateContentText(task.emission.title, task.emission.contact.name),
dueDate: task.emission.dueDate,
}));

notifications = orderBy(notifications, ['authoredOn'], ['desc']);
notifications = notifications.slice(0, MAX_NOTIFICATIONS);
latestNotificationTimestamp = notifications[0]?.authoredOn ?? latestNotificationTimestamp;
window.localStorage.setItem(LATEST_NOTIFICATION_TIMESTAMP, String(latestNotificationTimestamp));
return notifications;

} catch (exception) {
console.error('fetchNotifications(): Error fetching tasks', exception);
return [];
}
}

private getLatestNotificationTimestamp(): number {
if (this.isNewDay()) {
return 0;
}
return Number(window.localStorage.getItem(LATEST_NOTIFICATION_TIMESTAMP));
}

private isNewDay(): boolean {
const now = moment();
const timestampToday = Number(window.localStorage.getItem(TODAY_TIMESTAMP));
if (!now.isSame(timestampToday, 'day')) {
window.localStorage.setItem(TODAY_TIMESTAMP, String(moment().startOf('day').valueOf()));
return true;
}
return false;
}

private translateContentText(task: string, contact: string): string {
const key = 'android.notification.tasks.contentText';
return this.translateService.instant(key, { task, contact });
}

async get(): Promise<Notification[]> {
return Promise.race([
this.dbSyncService.sync(),
new Promise(resolve => setTimeout(() => resolve([]), 5 * 1000))
]).then(() => {
return this.fetchNotifications();
}).catch((error) => {
console.error('get(): notifications error syncing db', error);
return this.fetchNotifications();
});
}
}
16 changes: 16 additions & 0 deletions webapp/tests/karma/ts/services/android-api.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { GeolocationService } from '@mm-services/geolocation.service';
import { MRDTService } from '@mm-services/mrdt.service';
import { NavigationService } from '@mm-services/navigation.service';
import { AndroidAppLauncherService } from '@mm-services/android-app-launcher.service';
import { TranslateService } from '@mm-services/translate.service';
import { RulesEngineService } from '@mm-services/rules-engine.service';
import { DBSyncService } from '@mm-services/db-sync.service';

describe('AndroidApi service', () => {

Expand All @@ -19,6 +22,9 @@ describe('AndroidApi service', () => {
let consoleErrorMock;
let navigationService;
let androidAppLauncherService;
let translateService;
let rulesEngine;
let dbSyncService;

beforeEach(() => {
sessionService = {
Expand Down Expand Up @@ -47,13 +53,23 @@ describe('AndroidApi service', () => {

consoleErrorMock = sinon.stub(console, 'error');

translateService = {
get: sinon.stub().resolvesArg(0),
instant: sinon.stub().returnsArg(0),
};
rulesEngine = { };
dbSyncService = { sync: sinon.stub() };

TestBed.configureTestingModule({
providers: [
{ provide: SessionService, useValue: sessionService },
{ provide: GeolocationService, useValue: geolocationService },
{ provide: MRDTService, useValue: mrdtService },
{ provide: NavigationService, useValue: navigationService },
{ provide: AndroidAppLauncherService, useValue: androidAppLauncherService },
{ provide: TranslateService, useValue: translateService },
{ provide: RulesEngineService, useValue: rulesEngine },
{ provide: DBSyncService, useValue: dbSyncService },
],
});

Expand Down
Loading
Loading