mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merged PR 1932: feat(remission): ensure package assignment before completing return receipts
feat(remission): ensure package assignment before completing return receipts Add validation to check if a package is assigned to a return receipt before allowing completion. When no package is assigned, automatically open the package assignment dialog to let users scan/input a package number. - Add hasAssignedPackage input to complete component and pass from parent - Integrate RemissionStartService.assignPackage() in completion flow - Add assignPackageOnly flag to conditionally hide step counter in dialog - Update dialog data structure to support direct package assignment mode - Enhance test coverage for new assignment scenarios This ensures all completed return receipts have proper package tracking and improves the user workflow by guiding them through required steps. Ref: #5289
This commit is contained in:
committed by
Andreas Schickinger
parent
708ec01704
commit
fa8e601660
@@ -68,6 +68,7 @@
|
||||
<lib-remission-return-receipt-complete
|
||||
[returnId]="returnId()"
|
||||
[receiptId]="receiptId()"
|
||||
[hasAssignedPackage]="hasAssignedPackage()"
|
||||
[itemsLength]="items?.length"
|
||||
(reloadData)="returnResource.reload()"
|
||||
></lib-remission-return-receipt-complete>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RemissionReturnReceiptDetailsItemComponent } from './remission-return-r
|
||||
import { Location } from '@angular/common';
|
||||
import { createReturnResource } from './resources/return.resource';
|
||||
import {
|
||||
getPackageNumbersFromReturn,
|
||||
getReceiptItemsFromReturn,
|
||||
getReceiptNumberFromReturn,
|
||||
} from '@isa/remission/data-access';
|
||||
@@ -105,4 +106,9 @@ export class RemissionReturnReceiptDetailsComponent {
|
||||
const returnData = this.returnData();
|
||||
return !!returnData && !returnData.completed;
|
||||
});
|
||||
|
||||
hasAssignedPackage = computed(() => {
|
||||
const returnData = this.returnData();
|
||||
return getPackageNumbersFromReturn(returnData!) !== '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<span
|
||||
class="w-full flex items-center justify-center text-isa-neutral-900 isa-text-body-2-bold"
|
||||
>
|
||||
2/2
|
||||
</span>
|
||||
@if (!assignPackageOnly()) {
|
||||
<span
|
||||
class="w-full flex items-center justify-center text-isa-neutral-900 isa-text-body-2-bold"
|
||||
>
|
||||
2/2
|
||||
</span>
|
||||
}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="isa-text-subtitle-1-bold flex-shrink-0" data-what="title">
|
||||
Wannennummer Scannen
|
||||
|
||||
@@ -71,6 +71,9 @@ import { RequestStatus } from './remission-start-dialog.component';
|
||||
],
|
||||
})
|
||||
export class AssignPackageNumberComponent {
|
||||
/** Input flag indicating if the dialog is opened for package assignment only */
|
||||
assignPackageOnly = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Input signal containing the current request status for the assign package operation.
|
||||
* Used to display loading states and handle server-side validation errors.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@if (!assignPackageStepData()) {
|
||||
@if (!assignPackageStepData() && !data?.assignPackage) {
|
||||
<remi-create-return-receipt
|
||||
(createReturnReceipt)="onCreateReturnReceipt($event)"
|
||||
[createRemissionLoading]="createRemissionRequestStatus()"
|
||||
></remi-create-return-receipt>
|
||||
} @else {
|
||||
<remi-assign-package-number
|
||||
[assignPackageOnly]="!!data?.assignPackage"
|
||||
(assignPackageNumber)="onAssignPackageNumber($event)"
|
||||
[assignPackageLoading]="assignPackageRequestStatus()"
|
||||
></remi-assign-package-number>
|
||||
|
||||
@@ -59,6 +59,14 @@ export type RequestStatus = {
|
||||
export type RemissionStartDialogData = {
|
||||
/** The return group identifier for the remission process */
|
||||
returnGroup: string | undefined;
|
||||
|
||||
/** #5289 - Flag indicating if the dialog is opened for package assignment only */
|
||||
assignPackage?:
|
||||
| {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -220,17 +228,20 @@ export class RemissionStartDialogComponent extends DialogContentDirective<
|
||||
packageNumber: string | undefined,
|
||||
): Promise<void> {
|
||||
this.assignPackageRequestStatus.set({ loading: true });
|
||||
const data = this.assignPackageStepData();
|
||||
const data = this.assignPackageStepData() ?? this.data?.assignPackage;
|
||||
|
||||
if (!data || !packageNumber) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
const returnId = data.returnId;
|
||||
const receiptId = data.receiptId;
|
||||
|
||||
try {
|
||||
const response = await this.#remissionReturnReceiptService.assignPackage({
|
||||
packageNumber,
|
||||
returnId: data.returnId,
|
||||
receiptId: data.receiptId,
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
@@ -238,8 +249,8 @@ export class RemissionStartDialogComponent extends DialogContentDirective<
|
||||
}
|
||||
|
||||
this.onDialogClose({
|
||||
returnId: data.returnId,
|
||||
receiptId: data.receiptId,
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
this.assignPackageRequestStatus.set({ loading: false });
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -38,23 +38,120 @@ describe('RemissionStartService', () => {
|
||||
service = TestBed.inject(RemissionStartService);
|
||||
});
|
||||
|
||||
it('should start remission successfully when dialog returns result', async () => {
|
||||
// Arrange
|
||||
const returnGroup = 'test-return-group';
|
||||
describe('startRemission', () => {
|
||||
it('should start remission successfully when dialog returns result', async () => {
|
||||
// Arrange
|
||||
const returnGroup = 'test-return-group';
|
||||
|
||||
// Act
|
||||
await service.startRemission(returnGroup);
|
||||
// Act
|
||||
await service.startRemission(returnGroup);
|
||||
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: { returnGroup },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: { returnGroup },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.startRemission).toHaveBeenCalledWith({
|
||||
returnId: 'test-return-id',
|
||||
receiptId: 'test-receipt-id',
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.startRemission).toHaveBeenCalledWith({
|
||||
returnId: 'test-return-id',
|
||||
receiptId: 'test-receipt-id',
|
||||
it('should handle undefined returnGroup', async () => {
|
||||
// Arrange
|
||||
const returnGroup = undefined;
|
||||
|
||||
// Act
|
||||
await service.startRemission(returnGroup);
|
||||
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: { returnGroup: undefined },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.startRemission).toHaveBeenCalledWith({
|
||||
returnId: 'test-return-id',
|
||||
receiptId: 'test-receipt-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call startRemission when dialog returns falsy result', async () => {
|
||||
// Arrange
|
||||
const returnGroup = 'test-return-group';
|
||||
mockDialogRef.closed = of(null);
|
||||
|
||||
// Act
|
||||
await service.startRemission(returnGroup);
|
||||
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: { returnGroup },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.startRemission).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignPackage', () => {
|
||||
it('should open dialog with correct assignPackage data and return result', async () => {
|
||||
// Arrange
|
||||
const returnId = 12345;
|
||||
const receiptId = 67890;
|
||||
const expectedResult = {
|
||||
returnId: 'test-return-id',
|
||||
receiptId: 'test-receipt-id',
|
||||
};
|
||||
mockDialogRef.closed = of(expectedResult);
|
||||
|
||||
// Act
|
||||
const result = await service.assignPackage({ returnId, receiptId });
|
||||
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: {
|
||||
returnGroup: undefined,
|
||||
assignPackage: {
|
||||
returnId,
|
||||
receiptId,
|
||||
},
|
||||
},
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(mockRemissionStore.startRemission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle null result from dialog', async () => {
|
||||
// Arrange
|
||||
const returnId = 12345;
|
||||
const receiptId = 67890;
|
||||
mockDialogRef.closed = of(null);
|
||||
|
||||
// Act
|
||||
const result = await service.assignPackage({ returnId, receiptId });
|
||||
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: {
|
||||
returnGroup: undefined,
|
||||
assignPackage: {
|
||||
returnId,
|
||||
receiptId,
|
||||
},
|
||||
},
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,4 +26,26 @@ export class RemissionStartService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// #5289 - Bei WBS ohne Wannennummer, soll man nur die Wannennummer generieren können
|
||||
async assignPackage({
|
||||
returnId,
|
||||
receiptId,
|
||||
}: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}) {
|
||||
const remissionStartDialogRef = this.#remissionStartDialog({
|
||||
data: {
|
||||
returnGroup: undefined,
|
||||
assignPackage: {
|
||||
returnId,
|
||||
receiptId,
|
||||
},
|
||||
},
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
return await firstValueFrom(remissionStartDialogRef.closed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ describe('RemissionReturnReceiptCompleteComponent', () => {
|
||||
|
||||
mockRemissionStartService = {
|
||||
startRemission: vi.fn(),
|
||||
assignPackage: vi.fn(),
|
||||
};
|
||||
|
||||
mockRouter = {
|
||||
@@ -108,6 +109,7 @@ describe('RemissionReturnReceiptCompleteComponent', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
fixture.componentRef.setInput('itemsLength', 5);
|
||||
fixture.componentRef.setInput('hasAssignedPackage', true);
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -121,6 +123,7 @@ describe('RemissionReturnReceiptCompleteComponent', () => {
|
||||
expect(component.returnId()).toBe(123);
|
||||
expect(component.receiptId()).toBe(456);
|
||||
expect(component.itemsLength()).toBe(5);
|
||||
expect(component.hasAssignedPackage()).toBe(true);
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -150,10 +153,9 @@ describe('RemissionReturnReceiptCompleteComponent', () => {
|
||||
});
|
||||
|
||||
describe('completeRemission', () => {
|
||||
it('should complete remission without return group', async () => {
|
||||
it('should complete remission with package already assigned and no return group', async () => {
|
||||
// Arrange
|
||||
const mockReturn = { id: 123, returnGroup: null };
|
||||
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
@@ -166,10 +168,114 @@ describe('RemissionReturnReceiptCompleteComponent', () => {
|
||||
|
||||
// Assert
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
expect(mockRemissionStartService.assignPackage).not.toHaveBeenCalled();
|
||||
expect(mockInjectConfirmationDialog).not.toHaveBeenCalled();
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should complete remission without package assigned and assign package successfully', async () => {
|
||||
// Arrange
|
||||
fixture.componentRef.setInput('hasAssignedPackage', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const mockReturn = { id: 123, returnGroup: null };
|
||||
mockRemissionStartService.assignPackage.mockResolvedValue(true);
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Act
|
||||
await component.completeRemission();
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionStartService.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should complete remission with return group and user confirms completion', async () => {
|
||||
// Arrange
|
||||
const mockReturn = { id: 123, returnGroup: 'RG001' };
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
mockRemissionReturnReceiptService.completeReturnGroup.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Mock dialog result with confirmed=true
|
||||
mockDialogRef.closed = of({ confirmed: true });
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Act
|
||||
await component.completeRemission();
|
||||
|
||||
// Assert
|
||||
expect(mockInjectConfirmationDialog).toHaveBeenCalledWith({
|
||||
title: 'Wanne abgeschlossen',
|
||||
width: '30rem',
|
||||
data: {
|
||||
message: expect.stringContaining('Legen Sie abschließend den'),
|
||||
closeText: 'Neue Wanne',
|
||||
confirmText: 'Beenden',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnGroup,
|
||||
).toHaveBeenCalledWith({
|
||||
returnGroup: 'RG001',
|
||||
});
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should complete remission with return group and user chooses new container', async () => {
|
||||
// Arrange
|
||||
const mockReturn = { id: 123, returnGroup: 'RG001' };
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
mockRemissionStartService.startRemission.mockResolvedValue(undefined);
|
||||
|
||||
// Mock dialog result with confirmed=false (user chose "Neue Wanne")
|
||||
mockDialogRef.closed = of({ confirmed: false });
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Act
|
||||
await component.completeRemission();
|
||||
|
||||
// Assert
|
||||
expect(mockInjectConfirmationDialog).toHaveBeenCalledWith({
|
||||
title: 'Wanne abgeschlossen',
|
||||
width: '30rem',
|
||||
data: {
|
||||
message: expect.stringContaining('Legen Sie abschließend den'),
|
||||
closeText: 'Neue Wanne',
|
||||
confirmText: 'Beenden',
|
||||
},
|
||||
});
|
||||
expect(mockRemissionStartService.startRemission).toHaveBeenCalledWith(
|
||||
'RG001',
|
||||
);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent multiple completion attempts', async () => {
|
||||
// Arrange
|
||||
component.completingRemission.set(true);
|
||||
|
||||
@@ -89,6 +89,13 @@ export class RemissionReturnReceiptCompleteComponent {
|
||||
*/
|
||||
itemsLength = input.required<number>();
|
||||
|
||||
/**
|
||||
* Required input indicating if there is at least one package assigned to the return.
|
||||
* @input
|
||||
* @required
|
||||
*/
|
||||
hasAssignedPackage = input.required<boolean>();
|
||||
|
||||
/**
|
||||
* Output event that emits when the list needs to be reloaded.
|
||||
* This is used to refresh the remission list after completing a return.
|
||||
@@ -128,7 +135,22 @@ export class RemissionReturnReceiptCompleteComponent {
|
||||
return;
|
||||
}
|
||||
this.completingRemission.set(true);
|
||||
|
||||
try {
|
||||
// #5289 - Ensure a package is assigned before completing the remission
|
||||
if (!this.hasAssignedPackage()) {
|
||||
const res = await this.#remissionStartService.assignPackage({
|
||||
returnId: this.returnId(),
|
||||
receiptId: this.receiptId(),
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
this.completingRemission.set(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete Remission Flow
|
||||
const completedReturn = await this.completeSingleReturnReceipt();
|
||||
const returnGroup = completedReturn?.returnGroup;
|
||||
|
||||
@@ -146,7 +168,7 @@ export class RemissionReturnReceiptCompleteComponent {
|
||||
const dialogResult = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (dialogResult?.confirmed) {
|
||||
// Beenden - Remission abschließen Flow
|
||||
// Beenden - Remission abschließen Flow - Return Group Abschließen
|
||||
await this.#remissionReturnReceiptService.completeReturnGroup({
|
||||
returnGroup,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user