Merged PR 1906: feat(remission-data-access, remission-start-dialog): refactor remission workf...

feat(remission-data-access, remission-start-dialog): refactor remission workflow to use createRemission API

Replace the startRemission method with separate createRemission and assignPackage operations.
The new implementation improves error handling and provides better separation of concerns
between return creation and package assignment steps.

Key changes:
- Add CreateRemission interface to models with support for validation error properties
- Replace startRemission with createRemission method that handles return and receipt creation
- Update service methods to return ResponseArgs objects with proper error handling
- Enhance dialog components with reactive error handling using Angular effects
- Add comprehensive server-side validation error display in form controls
- Separate package assignment into dedicated step with individual loading states
- Improve test coverage with proper mocking of new service methods

The refactored workflow provides better user feedback for validation errors and maintains
the existing two-step process while improving maintainability and error handling.

Ref: #5251
This commit is contained in:
Nino Righi
2025-08-05 10:42:45 +00:00
committed by Patrick Brix
parent cce15a2137
commit 2dbf7dda37
11 changed files with 910 additions and 291 deletions

View File

@@ -0,0 +1,20 @@
/**
* Interface representing the data required to create a remission.
*/
export interface CreateRemission {
/**
* The unique identifier of the return group.
*/
returnId: number;
/**
* The unique identifier of the receipt.
*/
receiptId: number;
/**
* Map of property names to error messages for validation failures
* Keys represent property names, values contain validation error messages
*/
invalidProperties?: Record<string, string>;
}

View File

@@ -15,4 +15,5 @@ export * from './supplier';
export * from './receipt-return-tuple';
export * from './receipt-return-suggestion-tuple';
export * from './value-tuple-sting-and-integer';
export * from './create-remission';
export * from './remission-item-source';

View File

@@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing';
import { RemissionReturnReceiptService } from './remission-return-receipt.service';
import { ReturnService } from '@generated/swagger/inventory-api';
import { RemissionStockService } from './remission-stock.service';
import { ResponseArgsError } from '@isa/common/data-access';
import { ResponseArgsError, ResponseArgs } from '@isa/common/data-access';
import {
Return,
Stock,
@@ -12,6 +12,7 @@ import {
ReceiptReturnTuple,
ReceiptReturnSuggestionTuple,
ReturnSuggestion,
CreateRemission,
} from '../models';
import { subDays } from 'date-fns';
import { of, throwError } from 'rxjs';
@@ -334,7 +335,7 @@ describe('RemissionReturnReceiptService', () => {
);
const params = { returnGroup: 'group-1' };
const result = await service.createReturn(params);
expect(result).toEqual(mockReturn);
expect(result).toEqual({ result: mockReturn, error: null });
expect(mockReturnService.ReturnCreateReturn).toHaveBeenCalledWith({
data: {
supplier: { id: 'supplier-1' },
@@ -377,7 +378,7 @@ describe('RemissionReturnReceiptService', () => {
it('should return undefined when result is undefined', async () => {
mockReturnService.ReturnCreateReturn.mockReturnValue(of({ error: null }));
const result = await service.createReturn({ returnGroup: undefined });
expect(result).toBeUndefined();
expect(result).toEqual({ error: null });
});
it('should handle observable errors', async () => {
@@ -411,7 +412,7 @@ describe('RemissionReturnReceiptService', () => {
);
const params = { returnId: 123, receiptNumber: 'ABC-123' };
const result = await service.createReceipt(params);
expect(result).toEqual(mockReceipt);
expect(result).toEqual({ result: mockReceipt, error: null });
expect(mockReturnService.ReturnCreateReceipt).toHaveBeenCalledWith({
returnId: 123,
data: {
@@ -452,7 +453,7 @@ describe('RemissionReturnReceiptService', () => {
returnId: 123,
receiptNumber: 'ABC-123',
});
expect(result).toBeUndefined();
expect(result).toEqual({ error: null });
});
it('should handle observable errors', async () => {
@@ -482,7 +483,7 @@ describe('RemissionReturnReceiptService', () => {
packageNumber: 'PKG-789',
};
const result = await service.assignPackage(params);
expect(result).toEqual(mockReceipt);
expect(result).toEqual({ result: mockReceipt, error: null });
expect(
mockReturnService.ReturnCreateAndAssignPackage,
).toHaveBeenCalledWith({
@@ -506,7 +507,11 @@ describe('RemissionReturnReceiptService', () => {
it('should throw ResponseArgsError when API returns error', async () => {
mockReturnService.ReturnCreateAndAssignPackage.mockReturnValue(
of({ error: 'API Error', result: null }),
of({
error: 'API Error',
message: 'Failed to assign package',
result: null,
}),
);
await expect(
service.assignPackage({
@@ -526,7 +531,7 @@ describe('RemissionReturnReceiptService', () => {
receiptId: 456,
packageNumber: 'PKG-789',
});
expect(result).toBeUndefined();
expect(result).toEqual({ error: null });
});
it('should handle observable errors', async () => {
@@ -1043,42 +1048,50 @@ describe('RemissionReturnReceiptService', () => {
).rejects.toThrow('Observable error');
});
});
describe('startRemission', () => {
const mockReturn: Return = { id: 123 } as Return;
const mockReceipt: Receipt = { id: 456 } as Receipt;
const mockAssignedPackage: any = {
id: 456,
packageNumber: 'PKG-789',
describe('createRemission', () => {
const mockReturnResponse: ResponseArgs<Return> = {
result: { id: 123 } as Return,
error: false,
invalidProperties: { returnGroup: 'Invalid group' },
};
const mockReceiptResponse: ResponseArgs<Receipt> = {
result: { id: 456 } as Receipt,
error: false,
invalidProperties: { receiptNumber: 'Invalid number' },
};
beforeEach(() => {
// Mock the internal methods that startRemission calls
jest.spyOn(service, 'createReturn').mockResolvedValue(mockReturn);
jest.spyOn(service, 'createReceipt').mockResolvedValue(mockReceipt);
// Mock the internal methods that createRemission calls
jest.spyOn(service, 'createReturn').mockResolvedValue(mockReturnResponse);
jest
.spyOn(service, 'assignPackage')
.mockResolvedValue(mockAssignedPackage);
.spyOn(service, 'createReceipt')
.mockResolvedValue(mockReceiptResponse);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should start remission successfully with all parameters', async () => {
it('should create remission successfully with all parameters', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
// Act
const result = await service.startRemission(params);
const result: CreateRemission | undefined =
await service.createRemission(params);
// Assert
expect(result).toEqual({
returnId: 123,
receiptId: 456,
invalidProperties: {
returnGroup: 'Invalid group',
receiptNumber: 'Invalid number',
},
});
expect(service.createReturn).toHaveBeenCalledWith({
@@ -1088,28 +1101,26 @@ describe('RemissionReturnReceiptService', () => {
returnId: 123,
receiptNumber: 'REC-001',
});
expect(service.assignPackage).toHaveBeenCalledWith({
returnId: 123,
receiptId: 456,
packageNumber: 'PKG-789',
});
});
it('should start remission successfully with undefined returnGroup and receiptNumber', async () => {
it('should create remission successfully with undefined parameters', async () => {
// Arrange
const params = {
returnGroup: undefined,
receiptNumber: undefined,
packageNumber: 'PKG-789',
};
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toEqual({
returnId: 123,
receiptId: 456,
invalidProperties: {
returnGroup: 'Invalid group',
receiptNumber: 'Invalid number',
},
});
expect(service.createReturn).toHaveBeenCalledWith({
@@ -1119,11 +1130,6 @@ describe('RemissionReturnReceiptService', () => {
returnId: 123,
receiptNumber: undefined,
});
expect(service.assignPackage).toHaveBeenCalledWith({
returnId: 123,
receiptId: 456,
packageNumber: 'PKG-789',
});
});
it('should return undefined when createReturn fails', async () => {
@@ -1131,13 +1137,12 @@ describe('RemissionReturnReceiptService', () => {
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
(service.createReturn as jest.Mock).mockResolvedValue(undefined);
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toBeUndefined();
@@ -1145,21 +1150,23 @@ describe('RemissionReturnReceiptService', () => {
returnGroup: 'group-1',
});
expect(service.createReceipt).not.toHaveBeenCalled();
expect(service.assignPackage).not.toHaveBeenCalled();
});
it('should return undefined when createReturn returns null', async () => {
it('should return undefined when createReturn returns null result', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
(service.createReturn as jest.Mock).mockResolvedValue(null);
(service.createReturn as jest.Mock).mockResolvedValue({
result: null,
error: false,
invalidProperties: {},
});
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toBeUndefined();
@@ -1167,7 +1174,6 @@ describe('RemissionReturnReceiptService', () => {
returnGroup: 'group-1',
});
expect(service.createReceipt).not.toHaveBeenCalled();
expect(service.assignPackage).not.toHaveBeenCalled();
});
it('should return undefined when createReceipt fails', async () => {
@@ -1175,13 +1181,12 @@ describe('RemissionReturnReceiptService', () => {
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
(service.createReceipt as jest.Mock).mockResolvedValue(undefined);
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toBeUndefined();
@@ -1192,21 +1197,23 @@ describe('RemissionReturnReceiptService', () => {
returnId: 123,
receiptNumber: 'REC-001',
});
expect(service.assignPackage).not.toHaveBeenCalled();
});
it('should return undefined when createReceipt returns null', async () => {
it('should return undefined when createReceipt returns null result', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
(service.createReceipt as jest.Mock).mockResolvedValue(null);
(service.createReceipt as jest.Mock).mockResolvedValue({
result: null,
error: false,
invalidProperties: {},
});
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toBeUndefined();
@@ -1217,90 +1224,6 @@ describe('RemissionReturnReceiptService', () => {
returnId: 123,
receiptNumber: 'REC-001',
});
expect(service.assignPackage).not.toHaveBeenCalled();
});
it('should throw error when createReturn throws', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
const createReturnError = new Error('Failed to create return');
(service.createReturn as jest.Mock).mockRejectedValue(createReturnError);
// Act & Assert
await expect(service.startRemission(params)).rejects.toThrow(
'Failed to create return',
);
expect(service.createReturn).toHaveBeenCalledWith({
returnGroup: 'group-1',
});
expect(service.createReceipt).not.toHaveBeenCalled();
expect(service.assignPackage).not.toHaveBeenCalled();
});
it('should throw error when createReceipt throws', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
const createReceiptError = new Error('Failed to create receipt');
(service.createReceipt as jest.Mock).mockRejectedValue(
createReceiptError,
);
// Act & Assert
await expect(service.startRemission(params)).rejects.toThrow(
'Failed to create receipt',
);
expect(service.createReturn).toHaveBeenCalledWith({
returnGroup: 'group-1',
});
expect(service.createReceipt).toHaveBeenCalledWith({
returnId: 123,
receiptNumber: 'REC-001',
});
expect(service.assignPackage).not.toHaveBeenCalled();
});
it('should throw error when assignPackage throws', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
const assignPackageError = new Error('Failed to assign package');
(service.assignPackage as jest.Mock).mockRejectedValue(
assignPackageError,
);
// Act & Assert
await expect(service.startRemission(params)).rejects.toThrow(
'Failed to assign package',
);
expect(service.createReturn).toHaveBeenCalledWith({
returnGroup: 'group-1',
});
expect(service.createReceipt).toHaveBeenCalledWith({
returnId: 123,
receiptNumber: 'REC-001',
});
expect(service.assignPackage).toHaveBeenCalledWith({
returnId: 123,
receiptId: 456,
packageNumber: 'PKG-789',
});
});
it('should handle empty string parameters', async () => {
@@ -1308,16 +1231,19 @@ describe('RemissionReturnReceiptService', () => {
const params = {
returnGroup: '',
receiptNumber: '',
packageNumber: 'PKG-789',
};
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toEqual({
returnId: 123,
receiptId: 456,
invalidProperties: {
returnGroup: 'Invalid group',
receiptNumber: 'Invalid number',
},
});
expect(service.createReturn).toHaveBeenCalledWith({
@@ -1327,44 +1253,88 @@ describe('RemissionReturnReceiptService', () => {
returnId: 123,
receiptNumber: '',
});
expect(service.assignPackage).toHaveBeenCalledWith({
returnId: 123,
receiptId: 456,
packageNumber: 'PKG-789',
});
});
it('should proceed even if assignPackage fails silently', async () => {
it('should merge invalidProperties from both createReturn and createReceipt', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
receiptNumber: 'REC-001',
packageNumber: 'PKG-789',
};
// Mock assignPackage to resolve with undefined (but not throw)
(service.assignPackage as jest.Mock).mockResolvedValue(undefined);
const returnResponseWithProps: ResponseArgs<Return> = {
result: { id: 123 } as Return,
error: false,
invalidProperties: {
returnGroup: 'Return group error',
field1: 'Error 1',
},
};
const receiptResponseWithProps: ResponseArgs<Receipt> = {
result: { id: 456 } as Receipt,
error: false,
invalidProperties: {
receiptNumber: 'Receipt number error',
field2: 'Error 2',
},
};
(service.createReturn as jest.Mock).mockResolvedValue(
returnResponseWithProps,
);
(service.createReceipt as jest.Mock).mockResolvedValue(
receiptResponseWithProps,
);
// Act
const result = await service.startRemission(params);
const result = await service.createRemission(params);
// Assert
expect(result).toEqual({
returnId: 123,
receiptId: 456,
invalidProperties: {
returnGroup: 'Return group error',
field1: 'Error 1',
receiptNumber: 'Receipt number error',
field2: 'Error 2',
},
});
});
expect(service.createReturn).toHaveBeenCalledWith({
it('should handle missing invalidProperties', async () => {
// Arrange
const params = {
returnGroup: 'group-1',
});
expect(service.createReceipt).toHaveBeenCalledWith({
returnId: 123,
receiptNumber: 'REC-001',
});
expect(service.assignPackage).toHaveBeenCalledWith({
};
const returnResponseNoProps: ResponseArgs<Return> = {
result: { id: 123 } as Return,
error: false,
};
const receiptResponseNoProps: ResponseArgs<Receipt> = {
result: { id: 456 } as Receipt,
error: false,
};
(service.createReturn as jest.Mock).mockResolvedValue(
returnResponseNoProps,
);
(service.createReceipt as jest.Mock).mockResolvedValue(
receiptResponseNoProps,
);
// Act
const result = await service.createRemission(params);
// Assert
expect(result).toEqual({
returnId: 123,
receiptId: 456,
packageNumber: 'PKG-789',
invalidProperties: {},
});
});
});

View File

@@ -1,6 +1,10 @@
import { inject, Injectable } from '@angular/core';
import { ReturnService } from '@generated/swagger/inventory-api';
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import {
ResponseArgs,
ResponseArgsError,
takeUntilAborted,
} from '@isa/common/data-access';
import { subDays } from 'date-fns';
import { firstValueFrom } from 'rxjs';
import { RemissionStockService } from './remission-stock.service';
@@ -20,6 +24,7 @@ import {
FetchRemissionReturnReceiptsSchema,
} from '../schemas';
import {
CreateRemission,
Receipt,
ReceiptReturnSuggestionTuple,
ReceiptReturnTuple,
@@ -184,23 +189,25 @@ export class RemissionReturnReceiptService {
}
/**
* Creates a new remission return with an optional receipt number.
* Uses CreateReturnSchema to validate parameters before making the request.
* Creates a new remission return with the specified parameters.
* Validates parameters using CreateReturnSchema before making the request.
*
* @async
* @param {CreateReturn} params - The parameters for creating the return
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
* @returns {Promise<Return | undefined>} The created return object if successful, undefined otherwise
* @returns {Promise<ResponseArgs<Return> | undefined>} The created return object if successful, undefined otherwise
* @throws {ResponseArgsError} When the API request fails
* @throws {z.ZodError} When parameter validation fails
*
* @example
* const newReturn = await service.createReturn({ returnGroup: 'group1' });
* const returnResponse = await service.createReturn({
* returnGroup: 'group1',
* });
*/
async createReturn(
params: CreateReturn,
abortSignal?: AbortSignal,
): Promise<Return | undefined> {
): Promise<ResponseArgs<Return> | undefined> {
this.#logger.debug('Create remission return', () => ({ params }));
const suppliers =
@@ -246,27 +253,27 @@ export class RemissionReturnReceiptService {
throw new ResponseArgsError(res);
}
const createdReturn = res?.result as Return | undefined;
const returnResponse = res as ResponseArgs<Return> | undefined;
this.#logger.debug('Successfully created return', () => ({
found: !!createdReturn,
found: !!returnResponse,
}));
return createdReturn;
return returnResponse;
}
/**
* Creates a new remission return receipt with the specified parameters.
* Validates parameters using CreateReceiptSchema before making the request.
* Validates parameters using CreateReceipt before making the request.
*
* @async
* @param {CreateReceipt} params - The parameters for creating the receipt
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
* @returns {Promise<Receipt | undefined>} The created receipt object if successful, undefined otherwise
* @returns {Promise<ResponseArgs<Receipt> | undefined>} The created receipt object if successful, undefined otherwise
* @throws {ResponseArgsError} When the API request fails
* @throws {z.ZodError} When parameter validation fails
*
* @example
* const receipt = await service.createReceipt({
* const receiptResponse = await service.createReceipt({
* returnId: 123,
* receiptNumber: 'ABC-123',
* });
@@ -274,7 +281,7 @@ export class RemissionReturnReceiptService {
async createReceipt(
params: CreateReceipt,
abortSignal?: AbortSignal,
): Promise<Receipt | undefined> {
): Promise<ResponseArgs<Receipt> | undefined> {
this.#logger.debug('Create remission return receipt', () => ({ params }));
const stock =
@@ -319,22 +326,22 @@ export class RemissionReturnReceiptService {
throw new ResponseArgsError(res);
}
const receipt = res?.result as Receipt | undefined;
const receiptResponse = res as ResponseArgs<Receipt> | undefined;
this.#logger.debug('Successfully created return receipt', () => ({
found: !!receipt,
found: !!receiptResponse,
}));
return receipt;
return receiptResponse;
}
/**
* Assigns a package number to an existing return receipt.
* Validates parameters using AssignPackageSchema before making the request.
* Assigns a package to the specified return receipt.
* Validates parameters using AssignPackage before making the request.
*
* @async
* @param {AssignPackage} params - The parameters for assigning the package number
* @param {AssignPackage} params - The parameters for assigning the package
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
* @returns {Promise<Receipt | undefined>} The updated receipt object if successful, undefined otherwise
* @returns {Promise<ResponseArgs<Receipt> | undefined>} The updated receipt object if successful, undefined otherwise
* @throws {ResponseArgsError} When the API request fails
* @throws {z.ZodError} When parameter validation fails
*
@@ -348,7 +355,7 @@ export class RemissionReturnReceiptService {
async assignPackage(
params: AssignPackage,
abortSignal?: AbortSignal,
): Promise<Receipt | undefined> {
): Promise<ResponseArgs<Receipt> | undefined> {
this.#logger.debug('Assign package to return receipt', () => ({ params }));
const { returnId, receiptId, packageNumber } = params;
@@ -382,12 +389,14 @@ export class RemissionReturnReceiptService {
throw new ResponseArgsError(res);
}
const receipt = res?.result as Receipt | undefined;
this.#logger.debug('Successfully assigned package', () => ({
found: !!receipt,
}));
const receiptWithAssignedPackageResponse = res as
| ResponseArgs<Receipt>
| undefined;
return receipt;
this.#logger.debug('Successfully assigned package', () => ({
found: !!receiptWithAssignedPackageResponse,
}));
return receiptWithAssignedPackageResponse;
}
async removeReturnItemFromReturnReceipt(params: {
@@ -645,76 +654,69 @@ export class RemissionReturnReceiptService {
}
/**
* Starts a new remission process by creating a return and receipt.
* Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
* Warenbegleitschein eröffnen
* Creates a remission by generating a return and receipt.
* Validates parameters using CreateRemissionSchema before making the request.
*
* @async
* @param {Object} params - The parameters for starting the remission
* @param {string | undefined} params.returnGroup - Optional group identifier for the return
* @param {string | undefined} params.receiptNumber - Optional receipt number
* @param {string} params.packageNumber - The package number to assign
* @returns {Promise<FetchRemissionReturnParams | undefined>} The created return and receipt identifiers if successful, undefined otherwise
* @param {CreateRemission} params - The parameters for creating the remission
* @returns {Promise<CreateRemission | undefined>} The created remission object if successful, undefined otherwise
* @throws {ResponseArgsError} When the API request fails
* @throws {z.ZodError} When parameter validation fails
*
* @example
* const remission = await service.startRemission({
* returnGroup: 'group1',
* receiptNumber: 'ABC-123',
* packageNumber: 'PKG-789',
* const remission = await service.createRemission({
* returnId: 123,
* receiptId: 456,
* });
*/
async startRemission({
async createRemission({
returnGroup,
receiptNumber,
packageNumber,
}: {
returnGroup: string | undefined;
receiptNumber: string | undefined;
packageNumber: string;
}): Promise<FetchRemissionReturnParams | undefined> {
this.#logger.debug('Starting remission', () => ({
}): Promise<CreateRemission | undefined> {
this.#logger.debug('Create remission', () => ({
returnGroup,
receiptNumber,
packageNumber,
}));
// Warenbegleitschein eröffnen
const createdReturn: Return | undefined = await this.createReturn({
returnGroup,
});
const createdReturn: ResponseArgs<Return> | undefined =
await this.createReturn({
returnGroup,
});
if (!createdReturn) {
if (!createdReturn || !createdReturn.result) {
this.#logger.error('Failed to create return for remission');
return;
}
// Warenbegleitschein eröffnen
const createdReceipt: Receipt | undefined = await this.createReceipt({
returnId: createdReturn.id,
receiptNumber,
});
const createdReceipt: ResponseArgs<Receipt> | undefined =
await this.createReceipt({
returnId: createdReturn.result.id,
receiptNumber,
});
if (!createdReceipt) {
if (!createdReceipt || !createdReceipt.result) {
this.#logger.error('Failed to create return receipt');
return;
}
// Wannennummer zuweisen
await this.assignPackage({
returnId: createdReturn.id,
receiptId: createdReceipt.id,
packageNumber,
});
const invalidProperties = {
...createdReturn.invalidProperties,
...createdReceipt.invalidProperties,
};
this.#logger.info('Successfully started remission', () => ({
returnId: createdReturn.id,
receiptId: createdReceipt.id,
this.#logger.info('Successfully created remission', () => ({
returnId: createdReturn.result.id,
receiptId: createdReceipt.result.id,
}));
return {
returnId: createdReturn.id,
receiptId: createdReceipt.id,
returnId: createdReturn.result.id,
receiptId: createdReceipt.result.id,
invalidProperties,
};
}

View File

@@ -40,6 +40,10 @@
@if (control?.errors?.['pattern']) {
<span>Die Wannennummmer muss 14-stellig sein</span>
}
@if (control.errors?.['invalidProperties']) {
<span>{{ control.errors.invalidProperties }}</span>
}
</ui-text-field-errors>
}
</ui-text-field-container>
@@ -71,8 +75,8 @@
color="primary"
data-what="button"
data-which="save"
[disabled]="control.invalid || creatingReturnReceipt()"
[pending]="creatingReturnReceipt()"
[disabled]="control.invalid || assignPackageLoading().loading"
[pending]="assignPackageLoading().loading"
type="button"
>
Speichern

View File

@@ -1,8 +1,10 @@
import {
ChangeDetectionStrategy,
Component,
effect,
input,
output,
untracked,
} from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
@@ -15,8 +17,43 @@ import {
TextFieldErrorsComponent,
} from '@isa/ui/input-controls';
import { ScannerButtonComponent } from '@isa/shared/scanner';
import { boolean } from 'zod';
import { RequestStatus } from './remission-start-dialog.component';
/**
* Component for assigning a package number in the remission process.
*
* This component provides the second step of the remission workflow, allowing users to:
* - Manually input a 14-digit package number
* - Scan a package number using the integrated scanner
* - Validate package number format (14-digit requirement)
* - Handle server-side validation errors
*
* The component uses reactive forms for input validation and handles server-side
* validation errors through the RequestStatus input. It emits the package number
* when the form is submitted successfully, or undefined if validation fails.
*
* @example
* ```html
* <remi-assign-package-number
* [assignPackageLoading]="requestStatus"
* (assignPackageNumber)="onAssignPackageNumber($event)"
* />
* ```
*
* @example
* ```typescript
* // Handling the output event
* onAssignPackageNumber(packageNumber: string | undefined): void {
* if (packageNumber) {
* // Package number provided and validated
* console.log('Package number:', packageNumber);
* } else {
* // Validation failed or user cancelled
* console.log('Package assignment cancelled or invalid');
* }
* }
* ```
*/
@Component({
selector: 'remi-assign-package-number',
templateUrl: './assign-package-number.component.html',
@@ -34,18 +71,119 @@ import { boolean } from 'zod';
],
})
export class AssignPackageNumberComponent {
creatingReturnReceipt = 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.
* When invalidProperties contains 'PackageNumber', the form control is updated with the error.
*/
assignPackageLoading = input<RequestStatus>({ loading: false });
/**
* Output signal emitted when user completes the package assignment step.
* Emits the validated package number string if successful, or undefined if validation fails
* or the user cancels the operation.
*/
assignPackageNumber = output<string | undefined>();
/**
* Form control for the package number input field.
*
* Validation rules:
* - Required: Package number must be provided
* - Pattern: Must be exactly 14 digits (/^\d{14}$/)
*
* The control also handles server-side validation errors received through
* the assignPackageLoading input signal.
*/
control = new FormControl<string | undefined>(undefined, {
validators: [Validators.required, Validators.pattern(/^\d{14}$/)],
});
onScan(value: string) {
/**
* Constructor sets up reactive error handling for server-side validation.
*
* Uses an effect to monitor the assignPackageLoading signal and automatically
* update the form control's error state when server validation fails for the
* PackageNumber field. The untracked wrapper prevents infinite loops while
* allowing the effect to respond to signal changes.
*/
constructor() {
effect(() => {
const status = this.assignPackageLoading();
untracked(() => {
if (status?.invalidProperties?.['PackageNumber']) {
this.control.setErrors({
invalidProperties: status?.invalidProperties?.['PackageNumber'],
});
} else {
this.control.setErrors(null);
}
});
});
}
/**
* Handles scanned package number input from the scanner component.
*
* When a valid value is scanned, it automatically populates the form control.
* This provides a convenient way for users to input package numbers without
* manual typing, reducing input errors and improving workflow efficiency.
*
* @param value - The scanned package number string
*
* @example
* ```html
* <isa-scanner-button (scanned)="onScan($event)" />
* ```
*/
onScan(value: string): void {
this.control.setValue(value);
}
onSave(value: string | undefined) {
/**
* Handles form submission and validation for package number assignment.
*
* This method performs comprehensive validation before emitting the result:
* 1. Checks if a request is currently in progress (prevents double submission)
* 2. Triggers form validation to ensure package number meets requirements
* 3. Validates that a value was actually provided and is not null/undefined
* 4. Emits the appropriate result based on validation outcome
*
* If validation fails or no value is provided, emits undefined to indicate
* the operation should be cancelled or retried. If validation passes,
* emits the validated package number for processing.
*
* @param value - The package number value to validate and process
*
* @example
* ```html
* <form (ngSubmit)="onSave(control.value)">
* <input [formControl]="control" />
* <button type="submit" [disabled]="assignPackageLoading().loading">
* Assign Package
* </button>
* </form>
* ```
*
* @example
* ```typescript
* // In parent component
* onAssignPackage(packageNumber: string | undefined): void {
* if (packageNumber) {
* // Process the validated package number
* this.remissionService.assignPackage(packageNumber);
* } else {
* // Handle validation failure or cancellation
* this.showError('Please provide a valid 14-digit package number');
* }
* }
* ```
*/
onSave(value: string | undefined): void {
if (this.assignPackageLoading().loading) {
return;
}
this.control.updateValueAndValidity();
if (

View File

@@ -57,6 +57,10 @@
@if (control.errors?.['pattern']) {
<span>Die Packstück ID muss 18-stellig sein</span>
}
@if (control.errors?.['invalidProperties']) {
<span>{{ control.errors.invalidProperties }}</span>
}
</ui-text-field-errors>
}
</ui-text-field-container>
@@ -87,7 +91,8 @@
color="primary"
data-what="button"
data-which="save"
[disabled]="control.invalid"
[disabled]="control.invalid || createRemissionLoading().loading"
[pending]="createRemissionLoading().loading"
(click)="
onSave({ type: ReturnReceiptResultType.Input, value: control.value })
"

View File

@@ -1,4 +1,11 @@
import { ChangeDetectionStrategy, Component, output } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
effect,
input,
output,
untracked,
} from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
@@ -11,10 +18,46 @@ import {
} from '@isa/ui/input-controls';
import { ScannerButtonComponent } from '@isa/shared/scanner';
import {
RequestStatus,
ReturnReceiptResult,
ReturnReceiptResultType,
} from './remission-start-dialog.component';
/**
* Component for creating a return receipt in the remission process.
*
* This component provides the first step of the remission workflow, allowing users to:
* - Generate a receipt number automatically
* - Manually input a custom receipt number
* - Scan a receipt number using the integrated scanner
* - Validate receipt number format (18-digit requirement)
*
* The component uses reactive forms for input validation and handles server-side
* validation errors through the RequestStatus input. It emits different result types
* based on user interaction (generate, input with value, or close).
*
* @example
* ```html
* <remi-create-return-receipt
* [createRemissionLoading]="requestStatus"
* (createReturnReceipt)="onCreateReturnReceipt($event)"
* />
* ```
*
* @example
* ```typescript
* // Handling the output event
* onCreateReturnReceipt(result: ReturnReceiptResult): void {
* if (result?.type === ReturnReceiptResultType.Generate) {
* // User chose to auto-generate
* } else if (result?.type === ReturnReceiptResultType.Input) {
* // User provided custom value: result.value
* } else {
* // User closed/cancelled
* }
* }
* ```
*/
@Component({
selector: 'remi-create-return-receipt',
templateUrl: './create-return-receipt.component.html',
@@ -33,27 +76,131 @@ import {
],
})
export class CreateReturnReceiptComponent {
/**
* Expose ReturnReceiptResultType enum to template for type checking and comparisons.
*/
ReturnReceiptResultType = ReturnReceiptResultType;
/**
* Input signal containing the current request status for the create remission operation.
* Used to display loading states and handle server-side validation errors.
* When invalidProperties contains 'receiptNumber', the form control is updated with the error.
*/
createRemissionLoading = input<RequestStatus>({ loading: false });
/**
* Output signal emitted when user completes the return receipt creation step.
* Emits different result types based on user action:
* - Generate: User chose automatic generation
* - Input: User provided a custom receipt number
* - Close: User cancelled or provided invalid input
*/
createReturnReceipt = output<ReturnReceiptResult>();
/**
* Form control for the receipt number input field.
*
* Validation rules:
* - Required: Receipt number must be provided when using manual input
* - Pattern: Must be exactly 18 digits (/^\d{18}$/)
*
* The control also handles server-side validation errors received through
* the createRemissionLoading input signal.
*/
control = new FormControl<string | undefined>(undefined, {
validators: [Validators.required, Validators.pattern(/^\d{18}$/)],
});
onScan(value: string | null) {
/**
* Constructor sets up reactive error handling for server-side validation.
*
* Uses an effect to monitor the createRemissionLoading signal and automatically
* update the form control's error state when server validation fails for the
* receiptNumber field. The untracked wrapper prevents infinite loops while
* allowing the effect to respond to signal changes.
*/
constructor() {
effect(() => {
const status = this.createRemissionLoading();
untracked(() => {
if (status?.invalidProperties?.['receiptNumber']) {
this.control.setErrors({
invalidProperties: status?.invalidProperties?.['receiptNumber'],
});
} else {
this.control.setErrors(null);
}
});
});
}
/**
* Handles scanned receipt number input from the scanner component.
*
* When a valid value is scanned, it automatically populates the form control.
* This provides a convenient way for users to input receipt numbers without
* manual typing, reducing input errors.
*
* @param value - The scanned receipt number, or null if scan failed/was cancelled
*
* @example
* ```html
* <isa-scanner-button (scanned)="onScan($event)" />
* ```
*/
onScan(value: string | null): void {
if (!value) {
return;
}
this.control.setValue(value);
}
onGenerate() {
/**
* Handles the generate receipt number action.
*
* Emits a Generate result type, indicating the user wants the system
* to automatically generate a receipt number rather than providing one manually.
* This is typically the preferred option for most users as it eliminates
* input errors and ensures uniqueness.
*
* @example
* ```html
* <button (click)="onGenerate()">Generate Receipt Number</button>
* ```
*/
onGenerate(): void {
return this.createReturnReceipt.emit({
type: ReturnReceiptResultType.Generate,
});
}
onSave(value: ReturnReceiptResult) {
/**
* Handles form submission and validation for manual receipt number input.
*
* This method performs comprehensive validation before emitting the result:
* 1. Checks if a request is currently in progress (prevents double submission)
* 2. Triggers form validation to ensure receipt number meets requirements
* 3. Validates that a value was actually provided
* 4. Emits the appropriate result based on validation outcome
*
* If validation fails or no value is provided, emits a Close result.
* If validation passes, emits the provided value for processing.
*
* @param value - The return receipt result to process (typically contains user input)
*
* @example
* ```html
* <form (ngSubmit)="onSave({ type: ReturnReceiptResultType.Input, value: control.value })">
* <input [formControl]="control" />
* <button type="submit">Save Receipt Number</button>
* </form>
* ```
*/
onSave(value: ReturnReceiptResult): void {
if (this.createRemissionLoading().loading) {
return;
}
this.control.updateValueAndValidity();
if (

View File

@@ -1,10 +1,11 @@
@if (!createReturnReceipt()) {
@if (!assignPackageStepData()) {
<remi-create-return-receipt
(createReturnReceipt)="onCreateReturnReceipt($event)"
[createRemissionLoading]="createRemissionRequestStatus()"
></remi-create-return-receipt>
} @else {
<remi-assign-package-number
(assignPackageNumber)="onAssignPackageNumber($event)"
[creatingReturnReceipt]="loadRequests()"
[assignPackageLoading]="assignPackageRequestStatus()"
></remi-assign-package-number>
}

View File

@@ -1,20 +1,30 @@
import './test-mocks'; // Import mocks before anything else
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemissionStartDialogComponent } from './remission-start-dialog.component';
import {
RemissionStartDialogComponent,
ReturnReceiptResultType,
} from './remission-start-dialog.component';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
import { DialogComponent } from '@isa/ui/dialog';
import { vi } from 'vitest';
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('RemissionStartDialogComponent', () => {
let component: RemissionStartDialogComponent;
let fixture: ComponentFixture<RemissionStartDialogComponent>;
let mockRemissionService: {
createRemission: ReturnType<typeof vi.fn>;
assignPackage: ReturnType<typeof vi.fn>;
};
let mockDialogRef: {
close: ReturnType<typeof vi.fn>;
};
beforeEach(async () => {
// Mock the dialog ref
const mockDialogRef = {
mockDialogRef = {
close: vi.fn(),
};
@@ -28,30 +38,28 @@ describe('RemissionStartDialogComponent', () => {
};
// Mock remission service
const mockRemissionService = {
createReturn: vi.fn().mockResolvedValue({ id: 1 }),
createReceipt: vi.fn().mockResolvedValue({
id: 1,
receiptNumber: '12345',
items: []
mockRemissionService = {
createRemission: vi.fn().mockResolvedValue({
returnId: 123,
receiptId: 456,
}),
assignPackage: vi.fn().mockResolvedValue({}),
};
await TestBed.configureTestingModule({
imports: [
RemissionStartDialogComponent,
],
imports: [RemissionStartDialogComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
{ provide: RemissionReturnReceiptService, useValue: mockRemissionService },
{
provide: RemissionReturnReceiptService,
useValue: mockRemissionService,
},
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DIALOG_DATA, useValue: mockDialogData },
{ provide: DialogComponent, useValue: mockDialogComponent },
],
})
.compileComponents();
}).compileComponents();
fixture = TestBed.createComponent(RemissionStartDialogComponent);
component = fixture.componentInstance;
@@ -61,4 +69,162 @@ describe('RemissionStartDialogComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
describe('onCreateReturnReceipt', () => {
it('should handle generate receipt type successfully', async () => {
// Arrange
const returnReceipt = { type: ReturnReceiptResultType.Generate } as const;
// Act
await component.onCreateReturnReceipt(returnReceipt);
// Assert
expect(mockRemissionService.createRemission).toHaveBeenCalledWith({
returnGroup: 'test-group',
receiptNumber: undefined,
});
expect(component.assignPackageStepData()).toEqual({
returnId: 123,
receiptId: 456,
});
expect(component.createRemissionRequestStatus().loading).toBe(false);
});
it('should handle input receipt type with custom receipt number', async () => {
// Arrange
const returnReceipt = {
type: ReturnReceiptResultType.Input,
value: 'CUSTOM-123',
};
// Act
await component.onCreateReturnReceipt(returnReceipt);
// Assert
expect(mockRemissionService.createRemission).toHaveBeenCalledWith({
returnGroup: 'test-group',
receiptNumber: 'CUSTOM-123',
});
expect(component.assignPackageStepData()).toEqual({
returnId: 123,
receiptId: 456,
});
expect(component.createRemissionRequestStatus().loading).toBe(false);
});
it('should handle input receipt type with null value', async () => {
// Arrange
const returnReceipt = {
type: ReturnReceiptResultType.Input,
value: null,
};
// Act
await component.onCreateReturnReceipt(returnReceipt);
// Assert
expect(mockRemissionService.createRemission).toHaveBeenCalledWith({
returnGroup: 'test-group',
receiptNumber: undefined,
});
expect(component.assignPackageStepData()).toEqual({
returnId: 123,
receiptId: 456,
});
});
it('should close dialog when receipt type is close', async () => {
// Arrange
const closeSpy = vi.spyOn(component, 'close');
const returnReceipt = { type: ReturnReceiptResultType.Close } as const;
// Act
await component.onCreateReturnReceipt(returnReceipt);
// Assert
expect(closeSpy).toHaveBeenCalledWith(undefined);
expect(mockRemissionService.createRemission).not.toHaveBeenCalled();
});
});
describe('onAssignPackageNumber', () => {
it('should assign package number successfully', async () => {
// Arrange
const packageNumber = 'PKG-789';
component.assignPackageStepData.set({
returnId: 123,
receiptId: 456,
});
const closeSpy = vi.spyOn(component, 'close');
// Act
await component.onAssignPackageNumber(packageNumber);
// Assert
expect(mockRemissionService.assignPackage).toHaveBeenCalledWith({
packageNumber: 'PKG-789',
returnId: 123,
receiptId: 456,
});
expect(closeSpy).toHaveBeenCalledWith({
returnId: 123,
receiptId: 456,
});
expect(component.assignPackageRequestStatus().loading).toBe(false);
});
it('should close dialog when no package number provided', async () => {
// Arrange
const closeSpy = vi.spyOn(component, 'close');
component.assignPackageStepData.set({
returnId: 123,
receiptId: 456,
});
// Act
await component.onAssignPackageNumber(undefined);
// Assert
expect(closeSpy).toHaveBeenCalledWith(undefined);
expect(mockRemissionService.assignPackage).not.toHaveBeenCalled();
});
it('should close dialog when no step data available', async () => {
// Arrange
const closeSpy = vi.spyOn(component, 'close');
component.assignPackageStepData.set(undefined);
// Act
await component.onAssignPackageNumber('PKG-789');
// Assert
expect(closeSpy).toHaveBeenCalledWith(undefined);
expect(mockRemissionService.assignPackage).not.toHaveBeenCalled();
});
});
describe('onDialogClose', () => {
it('should close dialog with result', () => {
// Arrange
const result = { returnId: 123, receiptId: 456 };
const closeSpy = vi.spyOn(component, 'close');
// Act
component.onDialogClose(result);
// Assert
expect(closeSpy).toHaveBeenCalledWith(result);
});
it('should close dialog with undefined result', () => {
// Arrange
const closeSpy = vi.spyOn(component, 'close');
// Act
component.onDialogClose(undefined);
// Assert
expect(closeSpy).toHaveBeenCalledWith(undefined);
});
});
});

View File

@@ -10,32 +10,93 @@ import { provideIcons } from '@ng-icons/core';
import { isaActionScanner } from '@isa/icons';
import { CreateReturnReceiptComponent } from './create-return-receipt.component';
import { AssignPackageNumberComponent } from './assign-package-number.component';
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
import {
CreateRemission,
RemissionReturnReceiptService,
} from '@isa/remission/data-access';
/**
* Enumeration of possible return receipt result types.
* Used to determine the action taken by the user in the return receipt creation step.
*/
export enum ReturnReceiptResultType {
/** User closed the dialog without action */
Close = 'close',
/** User chose to generate a receipt number automatically */
Generate = 'generate',
/** User chose to input a custom receipt number */
Input = 'input',
}
/**
* Union type representing the possible results from the return receipt creation step.
* Each variant corresponds to a different user action in the dialog.
*/
export type ReturnReceiptResult =
| { type: ReturnReceiptResultType.Close }
| { type: ReturnReceiptResultType.Generate }
| {
type: ReturnReceiptResultType.Input;
/** The custom receipt number entered by the user */
value: string | undefined | null;
}
| undefined;
/**
* Request status object used to track the state of asynchronous operations.
* Contains loading state and optional validation error information.
*/
export type RequestStatus = {
/** Whether the request is currently in progress */
loading: boolean;
/** Map of property names to error messages for validation failures */
invalidProperties?: Record<string, string>;
};
/**
* Input data required to initialize the remission start dialog.
*/
export type RemissionStartDialogData = {
/** The return group identifier for the remission process */
returnGroup: string | undefined;
};
/**
* Result data returned when the remission start dialog completes successfully.
*/
export type RemissionStartDialogResult = {
/** The unique identifier of the created return */
returnId: number;
/** The unique identifier of the created receipt */
receiptId: number;
};
/**
* Dialog component for initiating the remission process.
*
* This component manages a two-step workflow:
* 1. Create a return receipt (either generate automatically or accept manual input)
* 2. Assign a package number to the created return
*
* The component extends DialogContentDirective to provide dialog functionality
* and uses Angular signals for reactive state management.
*
* @example
* ```typescript
* // Opening the dialog
* const dialogRef = this.dialog.open(RemissionStartDialogComponent, {
* data: { returnGroup: 'RG123' }
* });
*
* // Handling the result
* dialogRef.afterClosed().subscribe(result => {
* if (result) {
* console.log('Return created:', result.returnId);
* console.log('Receipt created:', result.receiptId);
* }
* });
* ```
*/
@Component({
selector: 'remi-remission-start-dialog',
templateUrl: './remission-start-dialog.component.html',
@@ -48,65 +109,169 @@ export class RemissionStartDialogComponent extends DialogContentDirective<
RemissionStartDialogData,
RemissionStartDialogResult | undefined
> {
/** Service for handling remission and return receipt operations */
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
createReturnReceipt = signal<ReturnReceiptResult>(undefined);
loadRequests = signal<boolean>(false);
onCreateReturnReceipt(returnReceipt: ReturnReceiptResult) {
if (returnReceipt && returnReceipt.type !== ReturnReceiptResultType.Close) {
this.createReturnReceipt.set(returnReceipt);
} else {
this.onDialogClose(undefined);
}
}
/**
* Signal tracking the request status for the create remission operation.
* Used to show loading states and validation errors in the first step.
*/
createRemissionRequestStatus = signal<RequestStatus>({ loading: false });
/**
* Signal tracking the request status for the assign package operation.
* Used to show loading states and validation errors in the second step.
*/
assignPackageRequestStatus = signal<RequestStatus>({ loading: false });
/**
* Signal containing the data needed for the package assignment step.
* Set after successful return receipt creation and used in the second step.
*/
assignPackageStepData = signal<
Omit<CreateRemission, 'invalidProperties'> | undefined
>(undefined);
/**
* Handles the completion of the return receipt creation step.
*
* This method processes the user's choice regarding receipt creation:
* - If the user closes or cancels, the dialog is closed with no result
* - If the user chooses to generate or input a receipt, a remission is created
* - On successful creation, the workflow advances to the package assignment step
*
* @param returnReceipt - The result from the return receipt creation step
* @returns Promise that resolves when the operation completes
*
* @example
* ```typescript
* // Called from template when user completes first step
* onCreateReturnReceipt({ type: ReturnReceiptResultType.Generate });
* onCreateReturnReceipt({
* type: ReturnReceiptResultType.Input,
* value: 'CUSTOM-123'
* });
* ```
*/
async onCreateReturnReceipt(
returnReceipt: ReturnReceiptResult,
): Promise<void> {
this.createRemissionRequestStatus.set({ loading: true });
onAssignPackageNumber(packageNumber: string | undefined) {
const returnReceipt = this.createReturnReceipt();
if (
packageNumber &&
returnReceipt &&
returnReceipt.type !== ReturnReceiptResultType.Close
!returnReceipt ||
returnReceipt.type === ReturnReceiptResultType.Close
) {
let receiptNumber: string | undefined = undefined; // undefined -> Wird generiert;
if (
returnReceipt.type === ReturnReceiptResultType.Input &&
returnReceipt.value
) {
receiptNumber = returnReceipt.value;
}
this.startRemission({ receiptNumber, packageNumber });
} else {
this.onDialogClose(undefined);
} else {
try {
let receiptNumber: string | undefined = undefined; // undefined -> Wird generiert;
if (
returnReceipt.type === ReturnReceiptResultType.Input &&
returnReceipt.value
) {
receiptNumber = returnReceipt.value;
}
const response =
await this.#remissionReturnReceiptService.createRemission({
returnGroup: this.data.returnGroup,
receiptNumber,
});
if (!response) {
return this.onDialogClose(undefined);
}
this.assignPackageStepData.set({
returnId: response.returnId,
receiptId: response.receiptId,
});
this.createRemissionRequestStatus.set({ loading: false });
} catch (error: any) {
console.error('Error creating remission:', error);
if (error?.error?.invalidProperties) {
this.createRemissionRequestStatus.set({
loading: false,
invalidProperties: error?.error?.invalidProperties,
});
}
}
}
}
async startRemission({
receiptNumber,
packageNumber,
}: {
receiptNumber: string | undefined;
packageNumber: string;
}) {
this.loadRequests.set(true);
const response = await this.#remissionReturnReceiptService.startRemission({
returnGroup: this.data.returnGroup,
receiptNumber,
packageNumber,
});
/**
* Handles the completion of the package assignment step.
*
* This method assigns a package number to the previously created return
* and completes the remission start workflow. On successful assignment,
* the dialog closes with the return and receipt IDs.
*
* @param packageNumber - The package number to assign to the return
* @returns Promise that resolves when the operation completes
*
* @example
* ```typescript
* // Called from template when user completes second step
* onAssignPackageNumber('PKG-789');
* ```
*/
async onAssignPackageNumber(
packageNumber: string | undefined,
): Promise<void> {
this.assignPackageRequestStatus.set({ loading: true });
const data = this.assignPackageStepData();
if (!response) {
if (!data || !packageNumber) {
return this.onDialogClose(undefined);
}
this.onDialogClose({
returnId: response.returnId,
receiptId: response.receiptId,
});
try {
const response = await this.#remissionReturnReceiptService.assignPackage({
packageNumber,
returnId: data.returnId,
receiptId: data.receiptId,
});
if (!response) {
return this.onDialogClose(undefined);
}
this.onDialogClose({
returnId: data.returnId,
receiptId: data.receiptId,
});
this.assignPackageRequestStatus.set({ loading: false });
} catch (error: any) {
console.error('Error assigning package:', error);
if (error?.error?.invalidProperties) {
this.assignPackageRequestStatus.set({
loading: false,
invalidProperties: error?.error?.invalidProperties,
});
}
}
}
onDialogClose(result: RemissionStartDialogResult | undefined) {
/**
* Closes the dialog with the specified result.
*
* This method wraps the inherited close method from DialogContentDirective
* to provide a consistent interface for closing the dialog from various
* points in the workflow.
*
* @param result - The result to return to the dialog opener, or undefined if cancelled
*
* @example
* ```typescript
* // Close with successful result
* this.onDialogClose({ returnId: 123, receiptId: 456 });
*
* // Close without result (cancelled)
* this.onDialogClose(undefined);
* ```
*/
onDialogClose(result: RemissionStartDialogResult | undefined): void {
this.close(result);
this.loadRequests.set(false);
}
}