mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat(remission): enhance has-pending-remission-hint component with dynamic days threshold and loading/error handling
This commit is contained in:
@@ -118,14 +118,14 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
this.#logger.info('Fetching incomplete returns from API', () => ({
|
||||
stockId: assignedStock.id,
|
||||
startDate: subDays(new Date(), 30).toISOString(),
|
||||
startDate: subDays(new Date(), 7).toISOString(),
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnQueryReturns({
|
||||
stockId: assignedStock.id,
|
||||
queryToken: {
|
||||
input: { returncompleted: 'false' },
|
||||
start: subDays(new Date(), 30).toISOString(),
|
||||
start: subDays(new Date(), 7).toISOString(),
|
||||
eagerLoading: 3,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<span class="mt-[2px]"
|
||||
>Die Remissionsliste wurde seit 7 Tagen nicht bearbeitet</span
|
||||
>
|
||||
@if (hasOldIncompleteRemissions() && !isLoading()) {
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<span class="mt-[2px]">
|
||||
Die Remissionsliste wurde seit {{ daysThreshold() }} Tagen nicht bearbeitet
|
||||
</span>
|
||||
} @else if (error()) {
|
||||
<button (click)="retry()" class="text-isa-accent-red hover:underline">
|
||||
Fehler beim Laden. Erneut versuchen
|
||||
</button>
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
:host {
|
||||
@apply flex items-start gap-1 justify-start;
|
||||
@apply text-isa-accent-red isa-text-body-2-bold;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HasPendingRemissionHintComponent } from './has-pending-remission-hint.component';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
describe('HasPendingRemissionHintComponent', () => {
|
||||
let component: HasPendingRemissionHintComponent;
|
||||
let fixture: ComponentFixture<HasPendingRemissionHintComponent>;
|
||||
let mockRemissionService: jest.Mocked<RemissionReturnReceiptService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRemissionService = {
|
||||
fetchIncompletedRemissionReturnReceipts: jest.fn(),
|
||||
} as unknown as jest.Mocked<RemissionReturnReceiptService>;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HasPendingRemissionHintComponent],
|
||||
providers: [
|
||||
{ provide: RemissionReturnReceiptService, useValue: mockRemissionService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HasPendingRemissionHintComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('Happy Path Cases', () => {
|
||||
it('should show hint when there are incomplete remissions older than 7 days', async () => {
|
||||
// Arrange
|
||||
const oldRemission = {
|
||||
id: '1',
|
||||
created: subDays(new Date(), 10),
|
||||
};
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue([
|
||||
oldRemission,
|
||||
] as any);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeTruthy();
|
||||
expect(compiled.textContent).toContain(
|
||||
'Die Remissionsliste wurde seit 7 Tagen nicht bearbeitet',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show hint when there are no incomplete remissions', async () => {
|
||||
// Arrange
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue(
|
||||
[],
|
||||
);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeFalsy();
|
||||
expect(compiled.textContent.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should not show hint when all remissions are newer than 7 days', async () => {
|
||||
// Arrange
|
||||
const recentRemission = {
|
||||
id: '1',
|
||||
created: subDays(new Date(), 3),
|
||||
};
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue([
|
||||
recentRemission,
|
||||
] as any);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeFalsy();
|
||||
expect(compiled.textContent.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should use custom days threshold when provided', async () => {
|
||||
// Arrange
|
||||
const remissionOlderThan5Days = {
|
||||
id: '1',
|
||||
created: subDays(new Date(), 6),
|
||||
};
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue([
|
||||
remissionOlderThan5Days,
|
||||
] as any);
|
||||
|
||||
// Act
|
||||
fixture.componentRef.setInput('daysThreshold', 5);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeTruthy();
|
||||
expect(compiled.textContent).toContain(
|
||||
'Die Remissionsliste wurde seit 5 Tagen nicht bearbeitet',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle mix of old and new remissions correctly', async () => {
|
||||
// Arrange
|
||||
const remissions = [
|
||||
{ id: '1', created: subDays(new Date(), 3) }, // Recent
|
||||
{ id: '2', created: subDays(new Date(), 10) }, // Old
|
||||
{ id: '3', created: subDays(new Date(), 5) }, // Recent
|
||||
];
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue(
|
||||
remissions as any,
|
||||
);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeTruthy();
|
||||
expect(compiled.textContent).toContain(
|
||||
'Die Remissionsliste wurde seit 7 Tagen nicht bearbeitet',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reload data when retry is called', async () => {
|
||||
// Arrange - First call returns old remission
|
||||
const oldRemission = {
|
||||
id: '1',
|
||||
created: subDays(new Date(), 10),
|
||||
};
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue([
|
||||
oldRemission,
|
||||
] as any);
|
||||
|
||||
// Act - Initial load
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert - Should show hint
|
||||
expect(fixture.nativeElement.querySelector('ng-icon')).toBeTruthy();
|
||||
|
||||
// Arrange - Setup for retry to return empty array
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue(
|
||||
[],
|
||||
);
|
||||
|
||||
// Act - Call retry
|
||||
component.retry();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert - Should not show hint after retry
|
||||
expect(fixture.nativeElement.querySelector('ng-icon')).toBeFalsy();
|
||||
expect(
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts,
|
||||
).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should show hint for remissions exactly at threshold', async () => {
|
||||
// Arrange - Remission exactly 7 days old
|
||||
const remissionAtThreshold = {
|
||||
id: '1',
|
||||
created: subDays(new Date(), 7),
|
||||
};
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue([
|
||||
remissionAtThreshold,
|
||||
] as any);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert - Should show hint (7 days ago is considered old)
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeTruthy();
|
||||
expect(compiled.textContent).toContain(
|
||||
'Die Remissionsliste wurde seit 7 Tagen nicht bearbeitet',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle remissions without created date gracefully', async () => {
|
||||
// Arrange
|
||||
const remissions = [
|
||||
{ id: '1', created: null },
|
||||
{ id: '2', created: undefined },
|
||||
{ id: '3', created: subDays(new Date(), 10) },
|
||||
];
|
||||
mockRemissionService.fetchIncompletedRemissionReturnReceipts.mockResolvedValue(
|
||||
remissions as any,
|
||||
);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert - Should still show hint due to the one old remission
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('ng-icon')).toBeTruthy();
|
||||
expect(compiled.textContent).toContain(
|
||||
'Die Remissionsliste wurde seit 7 Tagen nicht bearbeitet',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
resource,
|
||||
} from '@angular/core';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaOtherInfo } from '@isa/icons';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import { isAfter, subDays } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-has-pending-remission-hint',
|
||||
@@ -11,7 +20,60 @@ import { isaOtherInfo } from '@isa/icons';
|
||||
imports: [NgIcon],
|
||||
providers: [provideIcons({ isaOtherInfo })],
|
||||
host: {
|
||||
class: 'remi-has-pending-remission-hint ',
|
||||
class: 'remi-has-pending-remission-hint',
|
||||
},
|
||||
})
|
||||
export class HasPendingRemissionHintComponent {}
|
||||
export class HasPendingRemissionHintComponent {
|
||||
readonly #remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/**
|
||||
* Number of days after which remissions are considered old
|
||||
* @default 7
|
||||
*/
|
||||
readonly daysThreshold = input<number>(7);
|
||||
|
||||
/**
|
||||
* Resource that fetches incomplete remission return receipts
|
||||
*/
|
||||
readonly #incompleteRemissionsResource = resource({
|
||||
loader: ({ abortSignal }) =>
|
||||
this.#remissionReturnReceiptService.fetchIncompletedRemissionReturnReceipts(
|
||||
abortSignal,
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that determines if there are incomplete remissions older than the threshold
|
||||
*/
|
||||
readonly hasOldIncompleteRemissions = computed(() => {
|
||||
const remissions = this.#incompleteRemissionsResource.value();
|
||||
const threshold = this.daysThreshold();
|
||||
|
||||
if (!remissions || remissions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const thresholdDate = subDays(new Date(), threshold);
|
||||
return remissions.some((remission) => {
|
||||
if (!remission.created) return false;
|
||||
return isAfter(thresholdDate, remission.created);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Loading state of the resource
|
||||
*/
|
||||
readonly isLoading = this.#incompleteRemissionsResource.isLoading;
|
||||
|
||||
/**
|
||||
* Error state of the resource
|
||||
*/
|
||||
readonly error = this.#incompleteRemissionsResource.error;
|
||||
|
||||
/**
|
||||
* Retry method to refetch data in case of error
|
||||
*/
|
||||
readonly retry = () => {
|
||||
this.#incompleteRemissionsResource.reload();
|
||||
};
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
ComponentRef,
|
||||
computed,
|
||||
Directive,
|
||||
effect,
|
||||
EmbeddedViewRef,
|
||||
inject,
|
||||
resource,
|
||||
TemplateRef,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import { isAfter, subDays } from 'date-fns';
|
||||
import { HasPendingRemissionHintComponent } from './has-pending-remission-hint.component';
|
||||
|
||||
@Directive({ selector: '[remiHasPendingRemissionHint]' })
|
||||
export class HasPendingRemissionHintDirective {
|
||||
#viewContainerRef = inject(ViewContainerRef);
|
||||
|
||||
#templateRef = inject(TemplateRef);
|
||||
|
||||
#embeddedViewRef: EmbeddedViewRef<unknown> | null = null;
|
||||
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
incompleteRemissionResource = resource({
|
||||
loader: ({ abortSignal }) =>
|
||||
this.#remissionReturnReceiptService.fetchIncompletedRemissionReturnReceipts(
|
||||
abortSignal,
|
||||
),
|
||||
});
|
||||
|
||||
hasIncompletedRemissionsOlderThan7Days = computed(() => {
|
||||
const incompleteRemissions = this.incompleteRemissionResource.value();
|
||||
if (!incompleteRemissions) return false;
|
||||
|
||||
return incompleteRemissions.some((remission) =>
|
||||
isAfter(subDays(new Date(), 7), remission.created!),
|
||||
);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => this.render());
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasPendingRemission = this.hasIncompletedRemissionsOlderThan7Days();
|
||||
|
||||
if (hasPendingRemission && !this.#embeddedViewRef) {
|
||||
this.#embeddedViewRef = this.#viewContainerRef.createEmbeddedView(
|
||||
this.#templateRef,
|
||||
);
|
||||
const componentRef: ComponentRef<HasPendingRemissionHintComponent> = this
|
||||
.#embeddedViewRef
|
||||
.rootNodes[0] as ComponentRef<HasPendingRemissionHintComponent>;
|
||||
componentRef.changeDetectorRef.detectChanges();
|
||||
} else if (!hasPendingRemission && this.#embeddedViewRef) {
|
||||
this.#viewContainerRef.clear();
|
||||
this.#embeddedViewRef = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
<remi-has-pending-remission-hint
|
||||
*remiHasPendingRemissionHint
|
||||
></remi-has-pending-remission-hint>
|
||||
<remi-has-pending-remission-hint></remi-has-pending-remission-hint>
|
||||
<remission-feature-remission-start-card></remission-feature-remission-start-card>
|
||||
|
||||
<remi-feature-remission-list-select></remi-feature-remission-list-select>
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
StockInfo,
|
||||
ReturnSuggestion,
|
||||
} from '@isa/remission/data-access';
|
||||
import { HasPendingRemissionHintDirective } from './has-pending-remission-hint/has-pending-remission-hint.directive';
|
||||
import { HasPendingRemissionHintComponent } from './has-pending-remission-hint/has-pending-remission-hint.component';
|
||||
|
||||
function querySettingsFactory() {
|
||||
@@ -68,7 +67,6 @@ function querySettingsFactory() {
|
||||
RemissionListItemComponent,
|
||||
RouterLink,
|
||||
IconButtonComponent,
|
||||
HasPendingRemissionHintDirective,
|
||||
HasPendingRemissionHintComponent,
|
||||
],
|
||||
host: {
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { QuerySettings } from '@isa/shared/filter';
|
||||
|
||||
/**
|
||||
* Query settings configuration for filtering and sorting return receipts.
|
||||
* Provides options to sort by date in ascending or descending order.
|
||||
* @constant
|
||||
*/
|
||||
export const RETURN_RECEIPT_QUERY_SETTINGS: QuerySettings = {
|
||||
filter: [],
|
||||
input: [],
|
||||
orderBy: [
|
||||
// {
|
||||
// by: 'completed',
|
||||
// label: 'Remissiondatum',
|
||||
// desc: true,
|
||||
// },
|
||||
// {
|
||||
// by: 'completed',
|
||||
// label: 'Remissiondatum',
|
||||
// desc: false,
|
||||
// },
|
||||
{
|
||||
by: 'created',
|
||||
label: 'Datum',
|
||||
desc: true,
|
||||
},
|
||||
{
|
||||
by: 'created',
|
||||
label: 'Datum',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
import { QuerySettings } from '@isa/shared/filter';
|
||||
|
||||
/**
|
||||
* Query settings configuration for filtering and sorting return receipts.
|
||||
* Provides options to sort by date in ascending or descending order.
|
||||
* @constant
|
||||
*/
|
||||
export const RETURN_RECEIPT_QUERY_SETTINGS: QuerySettings = {
|
||||
filter: [],
|
||||
input: [],
|
||||
orderBy: [
|
||||
// {
|
||||
// by: 'completed',
|
||||
// label: 'Remissiondatum',
|
||||
// desc: true,
|
||||
// },
|
||||
// {
|
||||
// by: 'completed',
|
||||
// label: 'Remissiondatum',
|
||||
// desc: false,
|
||||
// },
|
||||
{
|
||||
by: 'created',
|
||||
label: 'Datum',
|
||||
desc: true,
|
||||
},
|
||||
{
|
||||
by: 'created',
|
||||
label: 'Datum',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user