Merged PR 2049: feat(oms): add auto-refresh for open reward tasks

feat(oms): add auto-refresh for open reward tasks

Implements 5-minute polling to automatically update open reward tasks
without requiring manual page refresh. Uses reference counting to safely
handle multiple consumers starting/stopping the refresh.

Changes:
- Add startAutoRefresh/stopAutoRefresh methods with ref counting
- Add lifecycle hooks to carousel component for proper cleanup
- Add logging for debugging auto-refresh behavior
- Add refresh interval constant (5 minutes)

Closes #5463

Related work items: #5463
This commit is contained in:
Lorenz Hilpert
2025-11-24 15:46:17 +00:00
committed by Nino Righi
parent 8b852cbd7a
commit dc26c4de04
3 changed files with 170 additions and 76 deletions

View File

@@ -1,4 +1,11 @@
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
computed,
inject,
OnInit,
OnDestroy,
} from '@angular/core';
import { OpenRewardTasksResource } from '@isa/oms/data-access'; import { OpenRewardTasksResource } from '@isa/oms/data-access';
import { CarouselComponent } from '@isa/ui/carousel'; import { CarouselComponent } from '@isa/ui/carousel';
import { OpenTaskCardComponent } from './open-task-card.component'; import { OpenTaskCardComponent } from './open-task-card.component';
@@ -32,7 +39,7 @@ import { OpenTaskCardComponent } from './open-task-card.component';
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class OpenTasksCarouselComponent { export class OpenTasksCarouselComponent implements OnInit, OnDestroy {
/** /**
* Global resource managing open reward tasks data * Global resource managing open reward tasks data
*/ */
@@ -48,7 +55,7 @@ export class OpenTasksCarouselComponent {
const tasks = this.openTasksResource.tasks(); const tasks = this.openTasksResource.tasks();
const seenOrderIds = new Set<number>(); const seenOrderIds = new Set<number>();
return tasks.filter(task => { return tasks.filter((task) => {
if (!task.orderId || seenOrderIds.has(task.orderId)) { if (!task.orderId || seenOrderIds.has(task.orderId)) {
return false; return false;
} }
@@ -56,4 +63,21 @@ export class OpenTasksCarouselComponent {
return true; return true;
}); });
}); });
/**
* Initializes the component by loading open tasks and starting auto-refresh.
* Ensures the carousel displays current data when mounted.
*/
ngOnInit(): void {
this.openTasksResource.refresh();
this.openTasksResource.startAutoRefresh();
}
/**
* Cleanup lifecycle hook that stops auto-refresh polling.
* Prevents memory leaks by clearing the refresh interval.
*/
ngOnDestroy(): void {
this.openTasksResource.stopAutoRefresh();
}
} }

View File

@@ -1 +1,7 @@
export const OMS_DISPLAY_ORDERS_KEY = 'OMS-DATA-ACCESS.DISPLAY_ORDERS'; export const OMS_DISPLAY_ORDERS_KEY = 'OMS-DATA-ACCESS.DISPLAY_ORDERS';
/**
* Auto-refresh interval for open reward tasks polling.
* Set to 5 minutes (300,000 ms) to periodically check for new or completed tasks.
*/
export const OMS_OPEN_REWARD_TASK_REFRESH_INTERVAL_MS = 60 * 1000 * 5; // 5 minutes

View File

@@ -1,73 +1,137 @@
import { computed, inject, Injectable, resource, Signal } from '@angular/core'; import { computed, inject, Injectable, resource, Signal } from '@angular/core';
import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api'; import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
import { OpenRewardTasksService } from '../services/open-reward-tasks.service'; import { logger } from '@isa/core/logging';
import { OpenRewardTasksService } from '../services/open-reward-tasks.service';
/** import { OMS_OPEN_REWARD_TASK_REFRESH_INTERVAL_MS } from '../constants';
* Global resource for managing open reward distribution tasks (Prämienausgabe).
* /**
* Provides reactive access to unfinished reward orders across the application. * Global resource for managing open reward distribution tasks (Prämienausgabe).
* This resource is provided at root level to ensure a single shared instance *
* for both the side menu indicator and the reward catalog carousel. * Provides reactive access to unfinished reward orders across the application.
* * This resource is provided at root level to ensure a single shared instance
* @example * for both the side menu indicator and the reward catalog carousel.
* ```typescript *
* // In component * @example
* readonly openTasksResource = inject(OpenRewardTasksResource); * ```typescript
* readonly hasOpenTasks = computed(() => * // In component
* (this.openTasksResource.tasks()?.length ?? 0) > 0 * readonly openTasksResource = inject(OpenRewardTasksResource);
* ); * readonly hasOpenTasks = computed(() =>
* ``` * (this.openTasksResource.tasks()?.length ?? 0) > 0
*/ * );
@Injectable({ providedIn: 'root' }) * ```
export class OpenRewardTasksResource { */
#openRewardTasksService = inject(OpenRewardTasksService); @Injectable({ providedIn: 'root' })
export class OpenRewardTasksResource {
/** #logger = logger({ resource: 'OpenRewardTasksResource' });
* Internal resource that manages data fetching and caching
*/ /**
#resource = resource({ * Interval ID for auto-refresh timer. Null when not actively polling.
loader: async ({ abortSignal }): Promise<DBHOrderItemListItemDTO[]> => { */
return await this.#openRewardTasksService.getOpenRewardTasks( #interval: ReturnType<typeof setInterval> | null = null;
abortSignal,
); /**
}, * Reference counter tracking how many consumers have started auto-refresh.
defaultValue: [], * Used to prevent stopping the interval while other consumers still need it.
}); */
#refCount = 0;
/**
* Signal containing the array of open reward tasks. #openRewardTasksService = inject(OpenRewardTasksService);
* Returns empty array when loading or on error.
*/ /**
readonly tasks: Signal<readonly DBHOrderItemListItemDTO[]> = * Internal resource that manages data fetching and caching
this.#resource.value.asReadonly(); */
#resource = resource({
/** loader: async ({ abortSignal }): Promise<DBHOrderItemListItemDTO[]> => {
* Signal indicating whether data is currently being fetched return await this.#openRewardTasksService.getOpenRewardTasks(abortSignal);
*/ },
readonly loading: Signal<boolean> = this.#resource.isLoading; defaultValue: [],
});
/**
* Signal containing error message if fetch failed, otherwise null /**
*/ * Signal containing the array of open reward tasks.
readonly error = computed( * Returns empty array when loading or on error.
() => this.#resource.error()?.message ?? null, */
); readonly tasks: Signal<readonly DBHOrderItemListItemDTO[]> =
this.#resource.value.asReadonly();
/**
* Signal indicating whether there are any open tasks /**
*/ * Signal indicating whether data is currently being fetched
readonly hasOpenTasks = computed(() => (this.tasks()?.length ?? 0) > 0); */
readonly loading: Signal<boolean> = this.#resource.isLoading;
/**
* Signal containing the count of open tasks /**
*/ * Signal containing error message if fetch failed, otherwise null
readonly taskCount = computed(() => this.tasks()?.length ?? 0); */
readonly error = computed(() => this.#resource.error()?.message ?? null);
/**
* Manually refresh the open tasks data. /**
* Useful for updating after a task is completed. * Signal indicating whether there are any open tasks
*/ */
refresh(): void { readonly hasOpenTasks = computed(() => (this.tasks()?.length ?? 0) > 0);
this.#resource.reload();
} /**
} * Signal containing the count of open tasks
*/
readonly taskCount = computed(() => this.tasks()?.length ?? 0);
/**
* Manually refresh the open tasks data.
* Useful for updating after a task is completed.
*/
refresh(): void {
this.#resource.reload();
}
/**
* Starts automatic polling of open reward tasks.
* Refreshes data every 5 minutes to keep the task list up-to-date.
* Uses reference counting - multiple consumers can safely call this method.
* The interval only starts when the first consumer calls it.
*/
startAutoRefresh(): void {
this.#refCount++;
this.#logger.debug('Auto-refresh start requested', () => ({
refCount: this.#refCount,
intervalActive: this.#interval !== null,
}));
if (this.#interval) {
return; // Already running
}
this.#interval = setInterval(() => {
this.refresh();
}, OMS_OPEN_REWARD_TASK_REFRESH_INTERVAL_MS);
this.#logger.info('Auto-refresh started', () => ({
intervalMs: OMS_OPEN_REWARD_TASK_REFRESH_INTERVAL_MS,
refCount: this.#refCount,
}));
}
/**
* Stops automatic polling of open reward tasks.
* Uses reference counting - only actually stops when all consumers have called this.
* Should be called during component cleanup to prevent memory leaks.
*/
stopAutoRefresh(): void {
if (this.#refCount > 0) {
this.#refCount--;
}
this.#logger.debug('Auto-refresh stop requested', () => ({
refCount: this.#refCount,
intervalActive: this.#interval !== null,
}));
if (this.#refCount === 0 && this.#interval) {
clearInterval(this.#interval);
this.#interval = null;
this.#logger.info('Auto-refresh stopped', () => ({
reason: 'All consumers released',
}));
}
}
}