mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
committed by
Nino Righi
parent
8b852cbd7a
commit
dc26c4de04
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user