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 { CarouselComponent } from '@isa/ui/carousel';
import { OpenTaskCardComponent } from './open-task-card.component';
@@ -32,7 +39,7 @@ import { OpenTaskCardComponent } from './open-task-card.component';
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OpenTasksCarouselComponent {
export class OpenTasksCarouselComponent implements OnInit, OnDestroy {
/**
* Global resource managing open reward tasks data
*/
@@ -48,7 +55,7 @@ export class OpenTasksCarouselComponent {
const tasks = this.openTasksResource.tasks();
const seenOrderIds = new Set<number>();
return tasks.filter(task => {
return tasks.filter((task) => {
if (!task.orderId || seenOrderIds.has(task.orderId)) {
return false;
}
@@ -56,4 +63,21 @@ export class OpenTasksCarouselComponent {
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';
/**
* 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 { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
import { OpenRewardTasksService } from '../services/open-reward-tasks.service';
/**
* Global resource for managing open reward distribution tasks (Prämienausgabe).
*
* Provides reactive access to unfinished reward orders across the application.
* This resource is provided at root level to ensure a single shared instance
* for both the side menu indicator and the reward catalog carousel.
*
* @example
* ```typescript
* // In component
* readonly openTasksResource = inject(OpenRewardTasksResource);
* readonly hasOpenTasks = computed(() =>
* (this.openTasksResource.tasks()?.length ?? 0) > 0
* );
* ```
*/
@Injectable({ providedIn: 'root' })
export class OpenRewardTasksResource {
#openRewardTasksService = inject(OpenRewardTasksService);
/**
* Internal resource that manages data fetching and caching
*/
#resource = resource({
loader: async ({ abortSignal }): Promise<DBHOrderItemListItemDTO[]> => {
return await this.#openRewardTasksService.getOpenRewardTasks(
abortSignal,
);
},
defaultValue: [],
});
/**
* Signal containing the array of open reward tasks.
* Returns empty array when loading or on error.
*/
readonly tasks: Signal<readonly DBHOrderItemListItemDTO[]> =
this.#resource.value.asReadonly();
/**
* Signal indicating whether data is currently being fetched
*/
readonly loading: Signal<boolean> = this.#resource.isLoading;
/**
* Signal containing error message if fetch failed, otherwise null
*/
readonly error = computed(
() => this.#resource.error()?.message ?? null,
);
/**
* Signal indicating whether there are any open tasks
*/
readonly hasOpenTasks = computed(() => (this.tasks()?.length ?? 0) > 0);
/**
* 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();
}
}
import { computed, inject, Injectable, resource, Signal } from '@angular/core';
import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
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.
* This resource is provided at root level to ensure a single shared instance
* for both the side menu indicator and the reward catalog carousel.
*
* @example
* ```typescript
* // In component
* readonly openTasksResource = inject(OpenRewardTasksResource);
* readonly hasOpenTasks = computed(() =>
* (this.openTasksResource.tasks()?.length ?? 0) > 0
* );
* ```
*/
@Injectable({ providedIn: 'root' })
export class OpenRewardTasksResource {
#logger = logger({ resource: 'OpenRewardTasksResource' });
/**
* Interval ID for auto-refresh timer. Null when not actively polling.
*/
#interval: ReturnType<typeof setInterval> | null = null;
/**
* Reference counter tracking how many consumers have started auto-refresh.
* Used to prevent stopping the interval while other consumers still need it.
*/
#refCount = 0;
#openRewardTasksService = inject(OpenRewardTasksService);
/**
* Internal resource that manages data fetching and caching
*/
#resource = resource({
loader: async ({ abortSignal }): Promise<DBHOrderItemListItemDTO[]> => {
return await this.#openRewardTasksService.getOpenRewardTasks(abortSignal);
},
defaultValue: [],
});
/**
* Signal containing the array of open reward tasks.
* Returns empty array when loading or on error.
*/
readonly tasks: Signal<readonly DBHOrderItemListItemDTO[]> =
this.#resource.value.asReadonly();
/**
* Signal indicating whether data is currently being fetched
*/
readonly loading: Signal<boolean> = this.#resource.isLoading;
/**
* Signal containing error message if fetch failed, otherwise null
*/
readonly error = computed(() => this.#resource.error()?.message ?? null);
/**
* Signal indicating whether there are any open tasks
*/
readonly hasOpenTasks = computed(() => (this.tasks()?.length ?? 0) > 0);
/**
* 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',
}));
}
}
}