From b8396f300cc231ce395d570ede759a1fff55f23f Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Mon, 29 Jun 2026 14:20:27 +0300 Subject: [PATCH 1/4] update task notification settings --- .../src/ts/services/rules-engine.service.ts | 4 +- .../ts/services/task-notifications.service.ts | 40 +++++++++++++++---- .../task-notification.service.spec.ts | 13 +++--- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/webapp/src/ts/services/rules-engine.service.ts b/webapp/src/ts/services/rules-engine.service.ts index 65e66dfa20d..2df55c79583 100644 --- a/webapp/src/ts/services/rules-engine.service.ts +++ b/webapp/src/ts/services/rules-engine.service.ts @@ -42,9 +42,11 @@ interface TaskDoc { _id: string; emission?: any; owner: string; - stateHistory: Array<{ state: string, timestamp: number }>; + stateHistory: StateHistory; } +export type StateHistory = Array<{ state: string, timestamp: number }>; + @Injectable({ providedIn: 'root' }) diff --git a/webapp/src/ts/services/task-notifications.service.ts b/webapp/src/ts/services/task-notifications.service.ts index a4c369c2a39..f5227022ae2 100644 --- a/webapp/src/ts/services/task-notifications.service.ts +++ b/webapp/src/ts/services/task-notifications.service.ts @@ -3,7 +3,7 @@ import { Subscription } from 'rxjs'; import { debounce as _debounce } from 'lodash-es'; import * as moment from 'moment'; -import { RulesEngineService } from '@mm-services/rules-engine.service'; +import { RulesEngineService, StateHistory } from '@mm-services/rules-engine.service'; import { TranslateService } from '@mm-services/translate.service'; import { FormatDateService } from '@mm-services/format-date.service'; import { SettingsService } from '@mm-services/settings.service'; @@ -11,6 +11,8 @@ import { AuthService } from '@mm-services/auth.service'; import { orderByDueDateAndPriority } from '@mm-reducers/tasks'; const DEFAULT_MAX_NOTIFICATIONS = 8; +const DEFAULT_WINDOW_START_TIME = '08:00'; +const DEFAULT_WINDOW_END_TIME = '19:00'; export interface Notification { _id: string, @@ -21,6 +23,14 @@ export interface Notification { readyAt: number } +export type notificationSettings = { + maxNotifications: number; + notificationWindow: { + start: string; + end: string; + } +}; + @Injectable({ providedIn: 'root' }) @@ -64,8 +74,13 @@ export class TasksNotificationService implements OnDestroy { private async updateAndroidStore(): Promise { const notifications = await this.fetchNotifications(); - const maxNotifications = await this.getMaxNotificationSettings(); - globalThis?.medicmobile_android?.updateTaskNotificationStore(JSON.stringify(notifications), maxNotifications); + const { maxNotifications, notificationWindow } = await this.getNotificationSettings(); + globalThis?.medicmobile_android + ?.updateTaskNotificationStore( + JSON.stringify(notifications), + maxNotifications, + JSON.stringify(notificationWindow), + ); } private async fetchNotifications(): Promise { @@ -97,17 +112,28 @@ export class TasksNotificationService implements OnDestroy { } } - private getReadyStateTimestamp(stateHistory): number { + private getReadyStateTimestamp(stateHistory: StateHistory): number { const readyState = stateHistory.find(state => state.state === 'Ready'); return readyState ? readyState.timestamp : 0; } - private async getMaxNotificationSettings(): Promise { + private async getNotificationSettings(): Promise { const settings = await this.settingsService.get(); + const taskNotificationSettings: notificationSettings = { + maxNotifications: DEFAULT_MAX_NOTIFICATIONS, + notificationWindow: { + start: DEFAULT_WINDOW_START_TIME, + end: DEFAULT_WINDOW_END_TIME, + } + }; if (settings?.tasks?.max_task_notifications) { - return settings.tasks.max_task_notifications; + taskNotificationSettings.maxNotifications = settings.tasks.max_task_notifications; + } + + if (settings.tasks?.task_notification_window) { + taskNotificationSettings.notificationWindow = settings.tasks.task_notification_window; } - return DEFAULT_MAX_NOTIFICATIONS; + return taskNotificationSettings; } private translateContentText(taskTitle: string, contact: string): string { diff --git a/webapp/tests/karma/ts/services/task-notification.service.spec.ts b/webapp/tests/karma/ts/services/task-notification.service.spec.ts index 807b537ed35..f72ab6d0913 100644 --- a/webapp/tests/karma/ts/services/task-notification.service.spec.ts +++ b/webapp/tests/karma/ts/services/task-notification.service.spec.ts @@ -127,22 +127,25 @@ describe('TasksNotificationService', () => { expect(service).to.be.ok; }); - it('should initialize max notifications value from settings', async () => { + it('should initialize notifications values from settings', async () => { settingsService.get.resolves({ tasks: { - max_task_notifications: 20 + max_task_notifications: 20, + task_notification_window: { start: '10:00', end: '18:00' } } }); - const maxNotifications = await service.getMaxNotificationSettings(); + const { maxNotifications, notificationWindow } = await service.getNotificationSettings(); expect(settingsService.get.callCount).to.equal(1); expect(maxNotifications).to.equal(20); + expect(notificationWindow).to.deep.equal({ start: '10:00', end: '18:00' }); }); - it('should return default max notifications value if settings value is not valid', async () => { + it('should return default notification values if settings value is not valid', async () => { settingsService.get.resolves({ max_task_notifications: '2o' }); - const maxNotifications = await service.getMaxNotificationSettings(); + const { maxNotifications, notificationWindow } = await service.getNotificationSettings(); expect(settingsService.get.callCount).to.equal(1); expect(maxNotifications).to.equal(8); + expect(notificationWindow).to.deep.equal({start: '08:00', end: '19:00'}); }); it('should fetch notifications', async () => { From 840a7813c5a57c4ec62726ae88b10aa149ffe4c8 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 14:37:47 +0300 Subject: [PATCH 2/4] tests and clean up --- webapp/src/ts/app.component.ts | 4 +- .../ts/services/task-notifications.service.ts | 24 +++++---- webapp/tests/karma/ts/app.component.spec.ts | 13 +++++ .../task-notification.service.spec.ts | 49 ++++++++++++++++++- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts index 4bf8cb90095..ce8de6e37c1 100644 --- a/webapp/src/ts/app.component.ts +++ b/webapp/src/ts/app.component.ts @@ -349,7 +349,9 @@ export class AppComponent implements OnInit, AfterViewInit { } private initAndroidTaskNotifications() { - if (typeof globalThis?.medicmobile_android?.updateTaskNotificationStore === 'function') { + if (typeof globalThis?.medicmobile_android?.updateTaskNotificationStoreWithSettings === 'function'){ + this.taskNotificationService.initOnAndroid(true); + } else if (typeof globalThis?.medicmobile_android?.updateTaskNotificationStore === 'function') { this.taskNotificationService.initOnAndroid(); } } diff --git a/webapp/src/ts/services/task-notifications.service.ts b/webapp/src/ts/services/task-notifications.service.ts index f5227022ae2..ae5bbeeadbe 100644 --- a/webapp/src/ts/services/task-notifications.service.ts +++ b/webapp/src/ts/services/task-notifications.service.ts @@ -49,12 +49,12 @@ export class TasksNotificationService implements OnDestroy { this.subscription.unsubscribe(); } - async initOnAndroid() { + async initOnAndroid(hasSettings?: boolean) { if (!await this.isEnabled()) { return; } this.subscribeToRulesEngine(); - this.updateAndroidStore(); + this.updateAndroidStore(hasSettings); } private async isEnabled() { @@ -72,15 +72,19 @@ export class TasksNotificationService implements OnDestroy { this.subscription.add(rulesEngineSubscription); } - private async updateAndroidStore(): Promise { + private async updateAndroidStore(hasSettings?: boolean): Promise { const notifications = await this.fetchNotifications(); const { maxNotifications, notificationWindow } = await this.getNotificationSettings(); - globalThis?.medicmobile_android - ?.updateTaskNotificationStore( - JSON.stringify(notifications), - maxNotifications, - JSON.stringify(notificationWindow), - ); + if (hasSettings) { + globalThis?.medicmobile_android + ?.updateTaskNotificationStoreWithSettings( + JSON.stringify(notifications), + maxNotifications, + JSON.stringify(notificationWindow), + ); + return; + } + globalThis?.medicmobile_android?.updateTaskNotificationStore(JSON.stringify(notifications), maxNotifications); } private async fetchNotifications(): Promise { @@ -119,7 +123,7 @@ export class TasksNotificationService implements OnDestroy { private async getNotificationSettings(): Promise { const settings = await this.settingsService.get(); - const taskNotificationSettings: notificationSettings = { + const taskNotificationSettings: notificationSettings = { maxNotifications: DEFAULT_MAX_NOTIFICATIONS, notificationWindow: { start: DEFAULT_WINDOW_START_TIME, diff --git a/webapp/tests/karma/ts/app.component.spec.ts b/webapp/tests/karma/ts/app.component.spec.ts index c459922a850..639c2a2b5a5 100644 --- a/webapp/tests/karma/ts/app.component.spec.ts +++ b/webapp/tests/karma/ts/app.component.spec.ts @@ -340,6 +340,19 @@ describe('AppComponent', () => { expect(tasksNotificationService.initOnAndroid.callCount).to.equal(1); }); + it('should prioritize updateTaskNotificationStoreWithSettings', async () => { + window.medicmobile_android = { + updateTaskNotificationStoreWithSettings: sinon.stub().returns(true), + updateTaskNotificationStore: sinon.stub().returns(true), + }; + + await getComponent(); + await component.setupPromise; + + expect(tasksNotificationService.initOnAndroid.callCount).to.equal(1); + expect(tasksNotificationService.initOnAndroid.calledOnceWith(true)).to.equal(true); + }); + it('should show reload popup when service worker is updated', async () => { await getComponent(); await component.setupPromise; diff --git a/webapp/tests/karma/ts/services/task-notification.service.spec.ts b/webapp/tests/karma/ts/services/task-notification.service.spec.ts index f72ab6d0913..3375945f871 100644 --- a/webapp/tests/karma/ts/services/task-notification.service.spec.ts +++ b/webapp/tests/karma/ts/services/task-notification.service.spec.ts @@ -145,7 +145,7 @@ describe('TasksNotificationService', () => { const { maxNotifications, notificationWindow } = await service.getNotificationSettings(); expect(settingsService.get.callCount).to.equal(1); expect(maxNotifications).to.equal(8); - expect(notificationWindow).to.deep.equal({start: '08:00', end: '19:00'}); + expect(notificationWindow).to.deep.equal({ start: '08:00', end: '19:00' }); }); it('should fetch notifications', async () => { @@ -262,4 +262,51 @@ describe('TasksNotificationService', () => { expect(updateAndroidStoreStub.callCount).to.equal(0); }); + describe('updateAndroidStore', () => { + let androidApi: any; + + beforeEach(() => { + androidApi = { + updateTaskNotificationStore: sinon.stub(), + updateTaskNotificationStoreWithSettings: sinon.stub(), + }; + (globalThis as any).medicmobile_android = androidApi; + }); + + afterEach(() => { + delete (globalThis as any).medicmobile_android; + }); + + it('should update the android store without settings', async () => { + await service.updateAndroidStore(); + + expect(androidApi.updateTaskNotificationStore.callCount).to.equal(1); + expect(androidApi.updateTaskNotificationStoreWithSettings.callCount).to.equal(0); + + const [notificationsJson, maxNotifications] = androidApi.updateTaskNotificationStore.args[0]; + expect(JSON.parse(notificationsJson)).to.be.an('array').that.has.lengthOf(3); + expect(maxNotifications).to.equal(8); + }); + + it('should update the android store with settings', async () => { + settingsService.get.resolves({ + tasks: { + max_task_notifications: 20, + task_notification_window: { start: '10:00', end: '18:00' }, + }, + }); + + await service.updateAndroidStore(true); + + expect(androidApi.updateTaskNotificationStore.callCount).to.equal(0); + expect(androidApi.updateTaskNotificationStoreWithSettings.callCount).to.equal(1); + + const [notificationsJson, maxNotifications, windowJson] = + androidApi.updateTaskNotificationStoreWithSettings.args[0]; + expect(JSON.parse(notificationsJson)).to.be.an('array').that.has.lengthOf(3); + expect(maxNotifications).to.equal(20); + expect(JSON.parse(windowJson)).to.deep.equal({ start: '10:00', end: '18:00' }); + }); + }); + }); From 28d8362ed22b9015a19f5b8cfd0738f7739da8e6 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Wed, 1 Jul 2026 08:40:19 +0300 Subject: [PATCH 3/4] fix bug --- webapp/src/ts/app.component.ts | 8 +++++--- .../ts/services/task-notifications.service.ts | 20 +++++++++---------- webapp/tests/karma/ts/app.component.spec.ts | 10 ++-------- .../task-notification.service.spec.ts | 16 +++++++-------- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts index ce8de6e37c1..49afbfbf8b9 100644 --- a/webapp/src/ts/app.component.ts +++ b/webapp/src/ts/app.component.ts @@ -349,9 +349,11 @@ export class AppComponent implements OnInit, AfterViewInit { } private initAndroidTaskNotifications() { - if (typeof globalThis?.medicmobile_android?.updateTaskNotificationStoreWithSettings === 'function'){ - this.taskNotificationService.initOnAndroid(true); - } else if (typeof globalThis?.medicmobile_android?.updateTaskNotificationStore === 'function') { + const android = globalThis?.medicmobile_android; + if ( + typeof android?.updateTaskNotificationStoreWithSettings === 'function' || + typeof android?.updateTaskNotificationStore === 'function' + ) { this.taskNotificationService.initOnAndroid(); } } diff --git a/webapp/src/ts/services/task-notifications.service.ts b/webapp/src/ts/services/task-notifications.service.ts index ae5bbeeadbe..c81d5202265 100644 --- a/webapp/src/ts/services/task-notifications.service.ts +++ b/webapp/src/ts/services/task-notifications.service.ts @@ -49,12 +49,12 @@ export class TasksNotificationService implements OnDestroy { this.subscription.unsubscribe(); } - async initOnAndroid(hasSettings?: boolean) { + async initOnAndroid() { if (!await this.isEnabled()) { return; } this.subscribeToRulesEngine(); - this.updateAndroidStore(hasSettings); + this.updateAndroidStore(); } private async isEnabled() { @@ -72,16 +72,16 @@ export class TasksNotificationService implements OnDestroy { this.subscription.add(rulesEngineSubscription); } - private async updateAndroidStore(hasSettings?: boolean): Promise { + private async updateAndroidStore(): Promise { const notifications = await this.fetchNotifications(); const { maxNotifications, notificationWindow } = await this.getNotificationSettings(); - if (hasSettings) { - globalThis?.medicmobile_android - ?.updateTaskNotificationStoreWithSettings( - JSON.stringify(notifications), - maxNotifications, - JSON.stringify(notificationWindow), - ); + + if (globalThis?.medicmobile_android?.updateTaskNotificationStoreWithSettings) { + globalThis.medicmobile_android.updateTaskNotificationStoreWithSettings( + JSON.stringify(notifications), + maxNotifications, + JSON.stringify(notificationWindow), + ); return; } globalThis?.medicmobile_android?.updateTaskNotificationStore(JSON.stringify(notifications), maxNotifications); diff --git a/webapp/tests/karma/ts/app.component.spec.ts b/webapp/tests/karma/ts/app.component.spec.ts index 639c2a2b5a5..aeb33df6bc7 100644 --- a/webapp/tests/karma/ts/app.component.spec.ts +++ b/webapp/tests/karma/ts/app.component.spec.ts @@ -340,17 +340,11 @@ describe('AppComponent', () => { expect(tasksNotificationService.initOnAndroid.callCount).to.equal(1); }); - it('should prioritize updateTaskNotificationStoreWithSettings', async () => { - window.medicmobile_android = { - updateTaskNotificationStoreWithSettings: sinon.stub().returns(true), - updateTaskNotificationStore: sinon.stub().returns(true), - }; - + it('should init task notifications on android if updateTaskNotificationStoreWithSettings', async () => { + window.medicmobile_android = { updateTaskNotificationStoreWithSettings: sinon.stub().returns(true) }; await getComponent(); await component.setupPromise; - expect(tasksNotificationService.initOnAndroid.callCount).to.equal(1); - expect(tasksNotificationService.initOnAndroid.calledOnceWith(true)).to.equal(true); }); it('should show reload popup when service worker is updated', async () => { diff --git a/webapp/tests/karma/ts/services/task-notification.service.spec.ts b/webapp/tests/karma/ts/services/task-notification.service.spec.ts index 3375945f871..2b88686b238 100644 --- a/webapp/tests/karma/ts/services/task-notification.service.spec.ts +++ b/webapp/tests/karma/ts/services/task-notification.service.spec.ts @@ -266,10 +266,7 @@ describe('TasksNotificationService', () => { let androidApi: any; beforeEach(() => { - androidApi = { - updateTaskNotificationStore: sinon.stub(), - updateTaskNotificationStoreWithSettings: sinon.stub(), - }; + androidApi = {}; (globalThis as any).medicmobile_android = androidApi; }); @@ -277,18 +274,21 @@ describe('TasksNotificationService', () => { delete (globalThis as any).medicmobile_android; }); - it('should update the android store without settings', async () => { + it('should update the android store without settings when only the basic api is available', async () => { + androidApi.updateTaskNotificationStore = sinon.stub(); + await service.updateAndroidStore(); expect(androidApi.updateTaskNotificationStore.callCount).to.equal(1); - expect(androidApi.updateTaskNotificationStoreWithSettings.callCount).to.equal(0); const [notificationsJson, maxNotifications] = androidApi.updateTaskNotificationStore.args[0]; expect(JSON.parse(notificationsJson)).to.be.an('array').that.has.lengthOf(3); expect(maxNotifications).to.equal(8); }); - it('should update the android store with settings', async () => { + it('should update the android store with settings when the settings api is available', async () => { + androidApi.updateTaskNotificationStore = sinon.stub(); + androidApi.updateTaskNotificationStoreWithSettings = sinon.stub(); settingsService.get.resolves({ tasks: { max_task_notifications: 20, @@ -296,7 +296,7 @@ describe('TasksNotificationService', () => { }, }); - await service.updateAndroidStore(true); + await service.updateAndroidStore(); expect(androidApi.updateTaskNotificationStore.callCount).to.equal(0); expect(androidApi.updateTaskNotificationStoreWithSettings.callCount).to.equal(1); From e0efc26975b1268c6407b452537a123dac396dac Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Wed, 1 Jul 2026 10:02:13 +0300 Subject: [PATCH 4/4] pass settings obj --- webapp/src/ts/services/task-notifications.service.ts | 3 +-- .../karma/ts/services/task-notification.service.spec.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/webapp/src/ts/services/task-notifications.service.ts b/webapp/src/ts/services/task-notifications.service.ts index c81d5202265..1fec337b10c 100644 --- a/webapp/src/ts/services/task-notifications.service.ts +++ b/webapp/src/ts/services/task-notifications.service.ts @@ -79,8 +79,7 @@ export class TasksNotificationService implements OnDestroy { if (globalThis?.medicmobile_android?.updateTaskNotificationStoreWithSettings) { globalThis.medicmobile_android.updateTaskNotificationStoreWithSettings( JSON.stringify(notifications), - maxNotifications, - JSON.stringify(notificationWindow), + JSON.stringify({maxNotifications, ...notificationWindow}), ); return; } diff --git a/webapp/tests/karma/ts/services/task-notification.service.spec.ts b/webapp/tests/karma/ts/services/task-notification.service.spec.ts index 2b88686b238..44267312a61 100644 --- a/webapp/tests/karma/ts/services/task-notification.service.spec.ts +++ b/webapp/tests/karma/ts/services/task-notification.service.spec.ts @@ -301,11 +301,14 @@ describe('TasksNotificationService', () => { expect(androidApi.updateTaskNotificationStore.callCount).to.equal(0); expect(androidApi.updateTaskNotificationStoreWithSettings.callCount).to.equal(1); - const [notificationsJson, maxNotifications, windowJson] = + const [notificationsJson, settingsJson] = androidApi.updateTaskNotificationStoreWithSettings.args[0]; expect(JSON.parse(notificationsJson)).to.be.an('array').that.has.lengthOf(3); - expect(maxNotifications).to.equal(20); - expect(JSON.parse(windowJson)).to.deep.equal({ start: '10:00', end: '18:00' }); + expect(JSON.parse(settingsJson)).to.deep.equal({ + maxNotifications: 20, + start: '10:00', + end: '18:00' + }); }); });