mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
- feat(remission-data-access,remission-list,remission-return-receipt-details): improve remission list UX and persist store state - feat(remission-list, remission-data-access): implement resource-based receipt data fetching - Merge branch 'develop' into feature/5230-Feedback-Remi-Starten - feat(remission-data-access, remission-list, ui-dialog, remission-start-dialog): consolidate remission workflow and enhance dialog system - feat(remission-list-item): extract selection logic into dedicated component Refs: #5230 #5233
This commit is contained in:
committed by
Lorenz Hilpert
parent
f4b541c7c0
commit
c5182809ac
@@ -3,7 +3,15 @@ import { RemissionReturnReceiptService } from './remission-return-receipt.servic
|
||||
import { ReturnService } from '@generated/swagger/inventory-api';
|
||||
import { RemissionStockService } from './remission-stock.service';
|
||||
import { ResponseArgsError } from '@isa/common/data-access';
|
||||
import { Return, Stock, Receipt } from '../models';
|
||||
import {
|
||||
Return,
|
||||
Stock,
|
||||
Receipt,
|
||||
RemissionListType,
|
||||
ReceiptReturnTuple,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReturnSuggestion,
|
||||
} from '../models';
|
||||
import { subDays } from 'date-fns';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { RemissionSupplierService } from './remission-supplier.service';
|
||||
@@ -1078,4 +1086,514 @@ 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',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the internal methods that startRemission calls
|
||||
jest.spyOn(service, 'createReturn').mockResolvedValue(mockReturn);
|
||||
jest.spyOn(service, 'createReceipt').mockResolvedValue(mockReceipt);
|
||||
jest
|
||||
.spyOn(service, 'assignPackage')
|
||||
.mockResolvedValue(mockAssignedPackage);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should start 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);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
|
||||
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 start remission successfully with undefined returnGroup and receiptNumber', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: undefined,
|
||||
receiptNumber: undefined,
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: undefined,
|
||||
});
|
||||
expect(service.createReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptNumber: undefined,
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when createReturn fails', async () => {
|
||||
// Arrange
|
||||
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);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).not.toHaveBeenCalled();
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when createReturn returns null', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
(service.createReturn as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).not.toHaveBeenCalled();
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when createReceipt fails', async () => {
|
||||
// Arrange
|
||||
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);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when createReceipt returns null', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
(service.createReceipt as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
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 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 () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: '',
|
||||
receiptNumber: '',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: '',
|
||||
});
|
||||
expect(service.createReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptNumber: '',
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should proceed even if assignPackage fails silently', 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);
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remitItem', () => {
|
||||
const mockReturnTuple = {
|
||||
item1: {} as Receipt,
|
||||
item2: {} as Return,
|
||||
} as ReceiptReturnTuple;
|
||||
const mockReturnSuggestionTuple = {
|
||||
item1: {} as Receipt,
|
||||
item2: {} as ReturnSuggestion,
|
||||
} as ReceiptReturnSuggestionTuple;
|
||||
const baseAddItem = {
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(service, 'addReturnItem').mockResolvedValue(mockReturnTuple);
|
||||
jest
|
||||
.spyOn(service, 'addReturnSuggestionItem')
|
||||
.mockResolvedValue(mockReturnSuggestionTuple);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call addReturnSuggestionItem for Abteilung type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.remitItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockReturnSuggestionTuple);
|
||||
expect(service.addReturnSuggestionItem).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
expect(service.addReturnItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call addReturnItem for Pflicht type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.remitItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockReturnTuple);
|
||||
expect(service.addReturnItem).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnItemId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
expect(service.addReturnSuggestionItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined for unknown type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: 'Unknown' as RemissionListType,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.remitItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
expect(service.addReturnItem).not.toHaveBeenCalled();
|
||||
expect(service.addReturnSuggestionItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle addReturnSuggestionItem throwing error', async () => {
|
||||
// Arrange
|
||||
const error = new Error('API Error');
|
||||
(service.addReturnSuggestionItem as jest.Mock).mockRejectedValue(error);
|
||||
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.remitItem(params)).rejects.toThrow('API Error');
|
||||
expect(service.addReturnSuggestionItem).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle addReturnItem throwing error', async () => {
|
||||
// Arrange
|
||||
const error = new Error('API Error');
|
||||
(service.addReturnItem as jest.Mock).mockRejectedValue(error);
|
||||
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.remitItem(params)).rejects.toThrow('API Error');
|
||||
expect(service.addReturnItem).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnItemId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle addReturnSuggestionItem returning undefined', async () => {
|
||||
// Arrange
|
||||
(service.addReturnSuggestionItem as jest.Mock).mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.remitItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
expect(service.addReturnSuggestionItem).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle addReturnItem returning undefined', async () => {
|
||||
// Arrange
|
||||
(service.addReturnItem as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.remitItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
expect(service.addReturnItem).toHaveBeenCalledWith({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnItemId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Receipt,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReceiptReturnTuple,
|
||||
RemissionListType,
|
||||
} from '../models';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { RemissionSupplierService } from './remission-supplier.service';
|
||||
@@ -673,4 +674,125 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
return updatedReturnSuggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new remission process by creating a return and receipt.
|
||||
* Validates parameters using FetchRemissionReturnReceiptSchema 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
|
||||
* @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',
|
||||
* });
|
||||
*/
|
||||
async startRemission({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}: {
|
||||
returnGroup: string | undefined;
|
||||
receiptNumber: string | undefined;
|
||||
packageNumber: string;
|
||||
}): Promise<FetchRemissionReturnParams | undefined> {
|
||||
this.#logger.debug('Starting remission', () => ({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}));
|
||||
|
||||
// Warenbegleitschein eröffnen
|
||||
const createdReturn: Return | undefined = await this.createReturn({
|
||||
returnGroup,
|
||||
});
|
||||
|
||||
if (!createdReturn) {
|
||||
this.#logger.error('Failed to create return for remission');
|
||||
return;
|
||||
}
|
||||
|
||||
// Warenbegleitschein eröffnen
|
||||
const createdReceipt: Receipt | undefined = await this.createReceipt({
|
||||
returnId: createdReturn.id,
|
||||
receiptNumber,
|
||||
});
|
||||
|
||||
if (!createdReceipt) {
|
||||
this.#logger.error('Failed to create return receipt');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wannennummer zuweisen
|
||||
await this.assignPackage({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
packageNumber,
|
||||
});
|
||||
|
||||
this.#logger.info('Successfully started remission', () => ({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
}));
|
||||
|
||||
return {
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remits an item to the return receipt based on its type.
|
||||
* Determines whether to add a return item or return suggestion item based on the remission list type.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for remitting the item
|
||||
* @param {number} params.itemId - The ID of the item to remit
|
||||
* @param {AddReturnItem | AddReturnSuggestionItem} params.addItem - The item data to add
|
||||
* @param {RemissionListType} params.type - The type of remission list (Abteilung or Pflicht)
|
||||
* @returns {Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined>} The updated receipt and return tuple if successful, undefined otherwise
|
||||
*/
|
||||
async remitItem({
|
||||
itemId,
|
||||
addItem,
|
||||
type,
|
||||
}: {
|
||||
itemId: number;
|
||||
addItem:
|
||||
| Omit<AddReturnItem, 'returnItemId'>
|
||||
| Omit<AddReturnSuggestionItem, 'returnSuggestionId'>;
|
||||
type: RemissionListType;
|
||||
}): Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined> {
|
||||
// ReturnSuggestion
|
||||
if (type === RemissionListType.Abteilung) {
|
||||
return await this.addReturnSuggestionItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
returnSuggestionId: itemId,
|
||||
quantity: addItem.quantity,
|
||||
inStock: addItem.inStock,
|
||||
});
|
||||
}
|
||||
|
||||
// ReturnItem
|
||||
if (type === RemissionListType.Pflicht) {
|
||||
return await this.addReturnItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
returnItemId: itemId,
|
||||
quantity: addItem.quantity,
|
||||
inStock: addItem.inStock,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ export class RemissionSearchService {
|
||||
* const response = await service.fetchList({
|
||||
* assignedStockId: 'stock123',
|
||||
* supplierId: 'supplier456',
|
||||
* take: 20,
|
||||
* take: 250,
|
||||
* skip: 0,
|
||||
* orderBy: 'itemName'
|
||||
* });
|
||||
@@ -212,7 +212,7 @@ export class RemissionSearchService {
|
||||
this.#logger.info('Fetching remission list from API', () => ({
|
||||
stockId: parsed.assignedStockId,
|
||||
supplierId: parsed.supplierId,
|
||||
take: parsed.take,
|
||||
take: 250,
|
||||
skip: parsed.skip,
|
||||
}));
|
||||
|
||||
@@ -223,7 +223,7 @@ export class RemissionSearchService {
|
||||
filter: parsed.filter,
|
||||
input: parsed.input,
|
||||
orderBy: parsed.orderBy,
|
||||
take: parsed.take,
|
||||
take: 250,
|
||||
skip: parsed.skip,
|
||||
},
|
||||
});
|
||||
@@ -270,7 +270,7 @@ export class RemissionSearchService {
|
||||
* const departmentResponse = await service.fetchDepartmentList({
|
||||
* assignedStockId: 'stock123',
|
||||
* supplierId: 'supplier456',
|
||||
* take: 50,
|
||||
* take: 250,
|
||||
* skip: 0
|
||||
* });
|
||||
*
|
||||
@@ -289,7 +289,7 @@ export class RemissionSearchService {
|
||||
this.#logger.info('Fetching department remission list from API', () => ({
|
||||
stockId: parsed.assignedStockId,
|
||||
supplierId: parsed.supplierId,
|
||||
take: parsed.take,
|
||||
take: 250,
|
||||
skip: parsed.skip,
|
||||
}));
|
||||
|
||||
@@ -300,7 +300,7 @@ export class RemissionSearchService {
|
||||
filter: parsed.filter,
|
||||
input: parsed.input,
|
||||
orderBy: parsed.orderBy,
|
||||
take: parsed.take,
|
||||
take: 250,
|
||||
skip: parsed.skip,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { RemissionStore } from './remission.store';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { RemissionReturnReceiptService } from '../services';
|
||||
|
||||
describe('RemissionStore', () => {
|
||||
let store: InstanceType<typeof RemissionStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockRemissionReturnReceiptService = {
|
||||
fetchRemissionReturnReceipt: jest.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [RemissionStore],
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
RemissionStore,
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
],
|
||||
});
|
||||
store = TestBed.inject(RemissionStore);
|
||||
});
|
||||
|
||||
@@ -2,11 +2,15 @@ import {
|
||||
patchState,
|
||||
signalStore,
|
||||
withComputed,
|
||||
withHooks,
|
||||
withMethods,
|
||||
withProps,
|
||||
withState,
|
||||
} from '@ngrx/signals';
|
||||
import { ReturnItem, ReturnSuggestion } from '../models';
|
||||
import { computed } from '@angular/core';
|
||||
import { computed, inject, resource } from '@angular/core';
|
||||
import { UserStorageProvider, withStorage } from '@isa/core/storage';
|
||||
import { RemissionReturnReceiptService } from '../services';
|
||||
|
||||
/**
|
||||
* Union type representing items that can be selected for remission.
|
||||
@@ -22,12 +26,6 @@ interface RemissionState {
|
||||
returnId: number | undefined;
|
||||
/** The unique identifier for the receipt. Can only be set once. */
|
||||
receiptId: number | undefined;
|
||||
/** The receipt number associated with the remission. */
|
||||
receiptNumber: string | undefined;
|
||||
/** The total number of items in the remission receipt. */
|
||||
receiptItemsCount: number | undefined;
|
||||
/** The package number associated with the remission. */
|
||||
packageNumber: string | undefined;
|
||||
/** Map of selected remission items indexed by their ID */
|
||||
selectedItems: Record<number, RemissionItem>;
|
||||
/** Map of selected quantities for each remission item indexed by their ID */
|
||||
@@ -41,9 +39,6 @@ interface RemissionState {
|
||||
const initialState: RemissionState = {
|
||||
returnId: undefined,
|
||||
receiptId: undefined,
|
||||
receiptNumber: undefined,
|
||||
receiptItemsCount: undefined,
|
||||
packageNumber: undefined,
|
||||
selectedItems: {},
|
||||
selectedQuantity: {},
|
||||
};
|
||||
@@ -71,10 +66,65 @@ const initialState: RemissionState = {
|
||||
export const RemissionStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withState(initialState),
|
||||
withStorage('remission-data-access.remission-store', UserStorageProvider),
|
||||
withProps(
|
||||
(
|
||||
store,
|
||||
remissionReturnReceiptService = inject(RemissionReturnReceiptService),
|
||||
) => ({
|
||||
/**
|
||||
* Private resource for fetching the current remission receipt.
|
||||
*
|
||||
* This resource automatically tracks changes to returnId and receiptId from the store
|
||||
* and refetches the receipt data when either value changes. The resource returns
|
||||
* undefined when either ID is not set, preventing unnecessary HTTP requests.
|
||||
*
|
||||
* The resource uses the injected RemissionReturnReceiptService to fetch receipt data
|
||||
* and supports request cancellation via AbortSignal for proper cleanup.
|
||||
*
|
||||
* @private
|
||||
* @returns A resource instance that manages the receipt data fetching lifecycle
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Access the resource through computed signals
|
||||
* const receipt = computed(() => store._receiptResource.value());
|
||||
* const status = computed(() => store._receiptResource.status());
|
||||
* const error = computed(() => store._receiptResource.error());
|
||||
*
|
||||
* // Manually reload the resource
|
||||
* store._receiptResource.reload();
|
||||
* ```
|
||||
*
|
||||
* @see {@link https://angular.dev/guide/signals/resource} Angular Resource API documentation
|
||||
*/
|
||||
_receiptResource: resource({
|
||||
loader: async ({ abortSignal }) => {
|
||||
const receiptId = store.receiptId();
|
||||
const returnId = store.returnId();
|
||||
|
||||
if (!receiptId || !returnId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const receipt =
|
||||
await remissionReturnReceiptService.fetchRemissionReturnReceipt(
|
||||
{
|
||||
returnId,
|
||||
receiptId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
return receipt;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
withComputed((store) => ({
|
||||
remissionStarted: computed(
|
||||
() => store.returnId() !== undefined && store.receiptId() !== undefined,
|
||||
),
|
||||
receipt: computed(() => store._receiptResource.value()),
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
/**
|
||||
@@ -93,15 +143,9 @@ export const RemissionStore = signalStore(
|
||||
startRemission({
|
||||
returnId,
|
||||
receiptId,
|
||||
receiptNumber,
|
||||
receiptItemsCount,
|
||||
packageNumber,
|
||||
}: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
receiptNumber: string;
|
||||
receiptItemsCount: number;
|
||||
packageNumber: string;
|
||||
}) {
|
||||
if (store.returnId() !== undefined || store.receiptId() !== undefined) {
|
||||
throw new Error(
|
||||
@@ -111,10 +155,16 @@ export const RemissionStore = signalStore(
|
||||
patchState(store, {
|
||||
returnId,
|
||||
receiptId,
|
||||
receiptNumber,
|
||||
receiptItemsCount,
|
||||
packageNumber,
|
||||
});
|
||||
store._receiptResource.reload();
|
||||
store.storeState();
|
||||
},
|
||||
/**
|
||||
* Reloads the receipt resource.
|
||||
* This method should be called when the receipt data needs to be refreshed.
|
||||
*/
|
||||
reloadReceipt() {
|
||||
store._receiptResource.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -231,6 +281,7 @@ export const RemissionStore = signalStore(
|
||||
*/
|
||||
finishRemission() {
|
||||
patchState(store, initialState);
|
||||
store.storeState();
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<ui-checkbox appearance="bullet">
|
||||
<input
|
||||
type="checkbox"
|
||||
[ngModel]="itemSelected()"
|
||||
(ngModelChange)="setSelected($event)"
|
||||
(click)="$event.stopPropagation()"
|
||||
data-what="remission-item-selection-checkbox"
|
||||
[attr.data-which]="item()?.product?.ean"
|
||||
/>
|
||||
</ui-checkbox>
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RemissionItem, RemissionStore } from '@isa/remission/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-list-item-select',
|
||||
templateUrl: './remission-list-item-select.component.html',
|
||||
styleUrl: './remission-list-item-select.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, TextButtonComponent, CheckboxComponent],
|
||||
})
|
||||
export class RemissionListItemSelectComponent {
|
||||
/**
|
||||
* Store for managing selected remission quantities.
|
||||
* @private
|
||||
*/
|
||||
#store = inject(RemissionStore);
|
||||
|
||||
/**
|
||||
* The item to display in the list.
|
||||
* Can be either a ReturnItem or a ReturnSuggestion.
|
||||
*/
|
||||
item = input.required<RemissionItem>();
|
||||
|
||||
/**
|
||||
* Computes whether the current item is selected in the remission store.
|
||||
* Checks if the item's ID exists in the selected items collection.
|
||||
*/
|
||||
itemSelected = computed(() => {
|
||||
const itemId = this.item()?.id;
|
||||
return !!itemId && !!this.#store.selectedItems()?.[itemId];
|
||||
});
|
||||
|
||||
/**
|
||||
* Selects the current item in the remission store.
|
||||
* Updates the selected items and quantities based on the item's ID.
|
||||
*
|
||||
* @param selected - Whether the item should be selected or not
|
||||
*/
|
||||
setSelected(selected: boolean) {
|
||||
const itemId = this.item()?.id;
|
||||
if (itemId && selected) {
|
||||
this.#store.selectRemissionItem(itemId, this.item());
|
||||
}
|
||||
if (itemId && !selected) {
|
||||
this.#store.removeItem(itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
@let i = item();
|
||||
<ui-client-row data-what="remission-list-item" [attr.data-which]="i.id">
|
||||
<ui-client-row-content class="flex flex-row gap-6">
|
||||
<ui-client-row-content class="flex flex-row gap-6 justify-between">
|
||||
<remi-product-info
|
||||
[item]="i"
|
||||
[orientation]="remiProductInfoOrientation()"
|
||||
></remi-product-info>
|
||||
@if (mobileBreakpoint()) {
|
||||
<ui-checkbox class="self-start mt-4" appearance="bullet">
|
||||
<input
|
||||
type="checkbox"
|
||||
[ngModel]="itemSelected()"
|
||||
(ngModelChange)="setSelected($event)"
|
||||
(click)="$event.stopPropagation()"
|
||||
data-what="remission-item-selection-checkbox"
|
||||
[attr.data-which]="i?.product?.ean"
|
||||
/>
|
||||
</ui-checkbox>
|
||||
@if (displayActions() && mobileBreakpoint()) {
|
||||
<remi-feature-remission-list-item-select
|
||||
class="self-start mt-4"
|
||||
[item]="i"
|
||||
></remi-feature-remission-list-item-select>
|
||||
}
|
||||
</ui-client-row-content>
|
||||
<ui-item-row-data>
|
||||
@@ -37,21 +31,16 @@
|
||||
></remi-product-stock-info>
|
||||
</ui-item-row-data>
|
||||
|
||||
@if (showActionButtons()) {
|
||||
@if (displayActions()) {
|
||||
<ui-item-row-data
|
||||
class="justify-end desktop-small:justify-between col-end-last"
|
||||
>
|
||||
@if (!mobileBreakpoint()) {
|
||||
<ui-checkbox class="self-end mt-4" appearance="bullet">
|
||||
<input
|
||||
type="checkbox"
|
||||
[ngModel]="itemSelected()"
|
||||
(ngModelChange)="setSelected($event)"
|
||||
(click)="$event.stopPropagation()"
|
||||
data-what="remission-item-selection-checkbox"
|
||||
[attr.data-which]="i?.product?.ean"
|
||||
/>
|
||||
</ui-checkbox>
|
||||
<remi-feature-remission-list-item-select
|
||||
class="self-end mt-4"
|
||||
[item]="i"
|
||||
>
|
||||
</remi-feature-remission-list-item-select>
|
||||
}
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
:host {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.ui-client-row {
|
||||
@apply isa-desktop-l:grid-cols-4;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { RemissionListItemComponent } from './remission-list-item.component';
|
||||
import {
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
StockInfo,
|
||||
RemissionListType,
|
||||
RemissionStore,
|
||||
} from '@isa/remission/data-access';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductStockInfoComponent,
|
||||
} from '@isa/remission/shared/product';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
// --- Setup dynamic mocking for injectRemissionListType ---
|
||||
let remissionListTypeValue: RemissionListType = RemissionListType.Pflicht;
|
||||
@@ -18,6 +23,28 @@ jest.mock('../injects/inject-remission-list-type', () => ({
|
||||
injectRemissionListType: () => () => remissionListTypeValue,
|
||||
}));
|
||||
|
||||
// Mock the calculation functions to have predictable behavior
|
||||
jest.mock('@isa/remission/data-access', () => ({
|
||||
...jest.requireActual('@isa/remission/data-access'),
|
||||
calculateStockToRemit: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the RemissionStore
|
||||
const mockRemissionStore = {
|
||||
remissionStarted: signal(true),
|
||||
selectedQuantity: signal({}),
|
||||
updateRemissionQuantity: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock the dialog services
|
||||
const mockNumberInputDialog = jest.fn();
|
||||
const mockFeedbackDialog = jest.fn();
|
||||
|
||||
jest.mock('@isa/ui/dialog', () => ({
|
||||
injectNumberInputDialog: () => mockNumberInputDialog,
|
||||
injectFeedbackDialog: () => mockFeedbackDialog,
|
||||
}));
|
||||
|
||||
describe('RemissionListItemComponent', () => {
|
||||
let component: RemissionListItemComponent;
|
||||
let fixture: ComponentFixture<RemissionListItemComponent>;
|
||||
@@ -31,6 +58,7 @@ describe('RemissionListItemComponent', () => {
|
||||
({
|
||||
id: 1,
|
||||
predefinedReturnQuantity: 5,
|
||||
remainingQuantityInStock: 10,
|
||||
...overrides,
|
||||
}) as ReturnItem;
|
||||
|
||||
@@ -39,6 +67,7 @@ describe('RemissionListItemComponent', () => {
|
||||
): ReturnSuggestion =>
|
||||
({
|
||||
id: 1,
|
||||
remainingQuantityInStock: 10,
|
||||
returnItem: {
|
||||
data: {
|
||||
id: 1,
|
||||
@@ -51,7 +80,8 @@ describe('RemissionListItemComponent', () => {
|
||||
const createMockStockInfo = (overrides: Partial<StockInfo> = {}): StockInfo =>
|
||||
({
|
||||
id: 1,
|
||||
quantity: 100,
|
||||
inStock: 100,
|
||||
removedFromStock: 0,
|
||||
...overrides,
|
||||
}) as StockInfo;
|
||||
|
||||
@@ -61,6 +91,12 @@ describe('RemissionListItemComponent', () => {
|
||||
RemissionListItemComponent,
|
||||
MockComponent(ProductInfoComponent),
|
||||
MockComponent(ProductStockInfoComponent),
|
||||
MockComponent(RemissionListItemSelectComponent),
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: RemissionStore, useValue: mockRemissionStore },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -68,6 +104,17 @@ describe('RemissionListItemComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
mockRemissionStore.selectedQuantity.set({});
|
||||
mockRemissionStore.remissionStarted.set(true);
|
||||
|
||||
// Reset the mocked function to return 0 by default
|
||||
const { calculateStockToRemit } = require('@isa/remission/data-access');
|
||||
calculateStockToRemit.mockReturnValue(0);
|
||||
});
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
@@ -324,6 +371,7 @@ describe('RemissionListItemComponent', () => {
|
||||
setRemissionListType(RemissionListType.Abteilung);
|
||||
const mockSuggestion = {
|
||||
id: 1,
|
||||
remainingQuantityInStock: 10,
|
||||
returnItem: {
|
||||
data: null as any,
|
||||
},
|
||||
@@ -338,6 +386,7 @@ describe('RemissionListItemComponent', () => {
|
||||
it('should handle item with unexpected structure', () => {
|
||||
const unexpectedItem = {
|
||||
id: 1,
|
||||
remainingQuantityInStock: 10,
|
||||
// Missing both returnItem and predefinedReturnQuantity
|
||||
} as any;
|
||||
fixture.componentRef.setInput('item', unexpectedItem);
|
||||
|
||||
@@ -22,12 +22,13 @@ import {
|
||||
ProductStockInfoComponent,
|
||||
} from '@isa/remission/shared/product';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectFeedbackDialog, injectTextInputDialog } from '@isa/ui/dialog';
|
||||
import { injectFeedbackDialog, injectNumberInputDialog } from '@isa/ui/dialog';
|
||||
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { injectRemissionListType } from '../injects/inject-remission-list-type';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
|
||||
|
||||
/**
|
||||
* Component representing a single item in the remission list.
|
||||
@@ -57,6 +58,7 @@ import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
ClientRowImports,
|
||||
ItemRowDataImports,
|
||||
CheckboxComponent,
|
||||
RemissionListItemSelectComponent,
|
||||
],
|
||||
})
|
||||
export class RemissionListItemComponent {
|
||||
@@ -64,7 +66,7 @@ export class RemissionListItemComponent {
|
||||
* Dialog service for prompting the user to enter a remission quantity.
|
||||
* @private
|
||||
*/
|
||||
#dialog = injectTextInputDialog();
|
||||
#dialog = injectNumberInputDialog();
|
||||
|
||||
/**
|
||||
* Dialog service for providing feedback to the user.
|
||||
@@ -121,7 +123,7 @@ export class RemissionListItemComponent {
|
||||
|
||||
/**
|
||||
* Computes the predefined return quantity for the current item.
|
||||
* - For Abteilung (suggestion), uses the nested returnItem's predefined quantity.
|
||||
* - For Abteilung (suggestion), uses the item's return item data or calculates based on stock.
|
||||
* - For Pflicht (item), uses the item's predefined quantity.
|
||||
* - Returns 0 if not available.
|
||||
*/
|
||||
@@ -130,9 +132,15 @@ export class RemissionListItemComponent {
|
||||
|
||||
// ReturnSuggestion
|
||||
if (this.remissionListType() === RemissionListType.Abteilung) {
|
||||
const predefinedReturnQuantity = (item as ReturnSuggestion)?.returnItem
|
||||
?.data?.predefinedReturnQuantity;
|
||||
return (
|
||||
(item as ReturnSuggestion)?.returnItem?.data
|
||||
?.predefinedReturnQuantity ?? 0
|
||||
predefinedReturnQuantity ??
|
||||
calculateStockToRemit({
|
||||
availableStock: this.availableStock(),
|
||||
remainingQuantityInStock: this.remainingQuantityInStock(),
|
||||
}) ??
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,11 +153,13 @@ export class RemissionListItemComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes whether the change quantity button should be shown based on remission list type.
|
||||
* - For Pflicht, Abteilung, checks if predefined return quantity is greater than 0.
|
||||
* Computes whether the item has a predefined return quantity.
|
||||
* Returns true if the predefined quantity is greater than 0.
|
||||
*/
|
||||
showActionButtons = computed<boolean>(() => {
|
||||
return !!this.predefinedReturnQuantity() && this.#store.remissionStarted();
|
||||
displayActions = computed<boolean>(() => {
|
||||
return (
|
||||
this.predefinedReturnQuantity() > 0 && this.#store.remissionStarted()
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -171,15 +181,6 @@ export class RemissionListItemComponent {
|
||||
() => this.#store.selectedQuantity()?.[this.item().id!],
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes whether the current item is selected in the remission store.
|
||||
* Checks if the item's ID exists in the selected items collection.
|
||||
*/
|
||||
itemSelected = computed(() => {
|
||||
const itemId = this.item()?.id;
|
||||
return !!itemId && !!this.#store.selectedItems()?.[itemId];
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the stock to remit based on available stock, predefined return quantity,
|
||||
* and remaining quantity in stock.
|
||||
@@ -206,22 +207,6 @@ export class RemissionListItemComponent {
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Selects the current item in the remission store.
|
||||
* Updates the selected items and quantities based on the item's ID.
|
||||
*
|
||||
* @param selected - Whether the item should be selected or not
|
||||
*/
|
||||
setSelected(selected: boolean) {
|
||||
const itemId = this.item()?.id;
|
||||
if (itemId && selected) {
|
||||
this.#store.selectRemissionItem(itemId, this.item());
|
||||
}
|
||||
if (itemId && !selected) {
|
||||
this.#store.removeItem(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog to change the remission quantity for the current item.
|
||||
* Prompts the user for a new quantity and updates the store if valid.
|
||||
@@ -252,9 +237,9 @@ export class RemissionListItemComponent {
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
const itemId = this.item()?.id;
|
||||
const quantity = Number(result?.inputValue);
|
||||
const quantity = result?.inputValue;
|
||||
|
||||
if (itemId && quantity > 0) {
|
||||
if (itemId && quantity !== undefined && quantity > 0) {
|
||||
this.#store.updateRemissionQuantity(itemId, this.item(), quantity);
|
||||
this.#feedbackDialog({
|
||||
data: { message: 'Remi-Menge wurde geändert' },
|
||||
|
||||
@@ -20,14 +20,12 @@
|
||||
<div class="flex flex-col gap-4 w-full items-center justify-center">
|
||||
@for (item of items(); track item.id) {
|
||||
@defer (on viewport) {
|
||||
<a [routerLink]="['../', 'return', item.id]" class="w-full">
|
||||
<remi-feature-remission-list-item
|
||||
#listElement
|
||||
[item]="item"
|
||||
[stock]="getStockForItem(item)"
|
||||
[productGroupValue]="getProductGroupValueForItem(item)"
|
||||
></remi-feature-remission-list-item>
|
||||
</a>
|
||||
<remi-feature-remission-list-item
|
||||
#listElement
|
||||
[item]="item"
|
||||
[stock]="getStockForItem(item)"
|
||||
[productGroupValue]="getProductGroupValueForItem(item)"
|
||||
></remi-feature-remission-list-item>
|
||||
} @placeholder {
|
||||
<div class="h-[7.75rem] w-full flex items-center justify-center">
|
||||
<ui-icon-button
|
||||
@@ -44,7 +42,7 @@
|
||||
@if (remissionStarted()) {
|
||||
<ui-stateful-button
|
||||
class="fixed right-6 bottom-6"
|
||||
(click)="remitItems()"
|
||||
(clicked)="remitItems()"
|
||||
(action)="remitItems()"
|
||||
[(state)]="remitItemsState"
|
||||
defaultContent="Remittieren"
|
||||
@@ -57,8 +55,7 @@
|
||||
size="large"
|
||||
color="brand"
|
||||
[pending]="remitItemsInProgress()"
|
||||
[attr.disabled]="!hasSelectedItems()"
|
||||
[attr.aria-disabled]="!hasSelectedItems()"
|
||||
[disabled]="!hasSelectedItems()"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
untracked,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
provideFilter,
|
||||
withQuerySettingsFactory,
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
import { RemissionStartCardComponent } from './remission-start-card/remission-start-card.component';
|
||||
import { RemissionListSelectComponent } from './remission-list-select/remission-list-select.component';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
createRemissionInStockResource,
|
||||
createRemissionListResource,
|
||||
@@ -82,7 +81,6 @@ function querySettingsFactory() {
|
||||
FilterControlsPanelComponent,
|
||||
RemissionListSelectComponent,
|
||||
RemissionListItemComponent,
|
||||
RouterLink,
|
||||
IconButtonComponent,
|
||||
StatefulButtonComponent,
|
||||
],
|
||||
@@ -101,11 +99,6 @@ export class RemissionListComponent {
|
||||
|
||||
searchItemToRemitDialog = injectDialog(SearchItemToRemitDialogComponent);
|
||||
|
||||
/**
|
||||
* Signal for the current route URL segments.
|
||||
*/
|
||||
routeUrl = toSignal(this.route.url);
|
||||
|
||||
/**
|
||||
* FilterService instance for managing filter state and queries.
|
||||
* @private
|
||||
@@ -137,11 +130,6 @@ export class RemissionListComponent {
|
||||
*/
|
||||
restoreScrollPosition = injectRestoreScrollPosition();
|
||||
|
||||
/**
|
||||
* Signal containing the current route data snapshot.
|
||||
*/
|
||||
routeData = toSignal(this.route.data);
|
||||
|
||||
/**
|
||||
* Signal representing the currently selected remission list type.
|
||||
*/
|
||||
@@ -331,9 +319,17 @@ export class RemissionListComponent {
|
||||
searchTerm: this.#filterService.query()?.input['qs'] || '',
|
||||
isDepartment,
|
||||
},
|
||||
}).closed.subscribe((result) => {
|
||||
}).closed.subscribe(async (result) => {
|
||||
if (result) {
|
||||
this.remissionResource.reload();
|
||||
if (this.remissionStarted()) {
|
||||
for (const item of result) {
|
||||
if (item?.id) {
|
||||
this.#store.selectRemissionItem(item.id, item);
|
||||
}
|
||||
}
|
||||
await this.remitItems();
|
||||
}
|
||||
this.reloadListAndReceipt();
|
||||
this.searchTrigger.set('reload');
|
||||
}
|
||||
});
|
||||
@@ -358,34 +354,21 @@ export class RemissionListComponent {
|
||||
const inStock = this.getAvailableStockForItem(item);
|
||||
|
||||
if (returnId && receiptId) {
|
||||
// ReturnSuggestion
|
||||
if (
|
||||
this.selectedRemissionListType() === RemissionListType.Abteilung
|
||||
) {
|
||||
await this.#remissionReturnReceiptService.addReturnSuggestionItem({
|
||||
await this.#remissionReturnReceiptService.remitItem({
|
||||
itemId: remissionItemIdNumber,
|
||||
addItem: {
|
||||
returnId,
|
||||
receiptId,
|
||||
returnSuggestionId: remissionItemIdNumber,
|
||||
quantity,
|
||||
inStock,
|
||||
});
|
||||
}
|
||||
|
||||
// ReturnItem
|
||||
if (this.selectedRemissionListType() === RemissionListType.Pflicht) {
|
||||
await this.#remissionReturnReceiptService.addReturnItem({
|
||||
returnId,
|
||||
receiptId,
|
||||
returnItemId: remissionItemIdNumber,
|
||||
quantity,
|
||||
inStock,
|
||||
});
|
||||
}
|
||||
},
|
||||
type: this.selectedRemissionListType(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.remitItemsState.set('success');
|
||||
this.remissionResource.reload();
|
||||
this.reloadListAndReceipt();
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remit items', error);
|
||||
this.remitItemsError.set(
|
||||
@@ -399,4 +382,13 @@ export class RemissionListComponent {
|
||||
this.#store.clearSelectedItems();
|
||||
this.remitItemsInProgress.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the remission list and receipt data.
|
||||
* This method is used to refresh the displayed data after changes.
|
||||
*/
|
||||
reloadListAndReceipt() {
|
||||
this.remissionResource.reload();
|
||||
this.#store.reloadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<a
|
||||
class="remi-feature-remission-return-card__navigate-cta"
|
||||
data-which="navigate-to-receipt"
|
||||
data-what="navigate-to-receipt"
|
||||
uiButton
|
||||
color="secondary"
|
||||
size="large"
|
||||
(click)="navigateToReceipt()"
|
||||
[routerLink]="['../return-receipt', returnId(), receiptId()]"
|
||||
[relativeTo]="route"
|
||||
>
|
||||
Zum Warenbegleitschein
|
||||
</button>
|
||||
</a>
|
||||
|
||||
@@ -3,8 +3,9 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { RemissionStore } from '@isa/remission/data-access';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
@@ -13,25 +14,30 @@ import { ButtonComponent } from '@isa/ui/buttons';
|
||||
templateUrl: './remission-return-card.component.html',
|
||||
styleUrl: './remission-return-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ButtonComponent],
|
||||
imports: [RouterLink, ButtonComponent],
|
||||
})
|
||||
export class RemissionReturnCardComponent {
|
||||
#router = inject(Router);
|
||||
#route = inject(ActivatedRoute);
|
||||
route = inject(ActivatedRoute);
|
||||
#remissionStore = inject(RemissionStore);
|
||||
|
||||
receiptNumber = computed(() => {
|
||||
const receiptNumber = this.#remissionStore.receiptNumber();
|
||||
return receiptNumber?.substring(6, 12);
|
||||
returnId = computed(() => this.#remissionStore.returnId());
|
||||
receiptId = computed(() => this.#remissionStore.receiptId());
|
||||
|
||||
receiptItemsCount = computed(() => {
|
||||
const receipt = this.#remissionStore.receipt();
|
||||
return receipt?.items?.length ?? 0;
|
||||
});
|
||||
|
||||
receiptItemsCount = computed(() => this.#remissionStore.receiptItemsCount());
|
||||
receiptNumber = computed(() => {
|
||||
const receipt = this.#remissionStore.receipt();
|
||||
return receipt?.receiptNumber?.substring(6, 12);
|
||||
});
|
||||
|
||||
async navigateToReceipt() {
|
||||
const returnId = this.#remissionStore.returnId();
|
||||
const receiptId = this.#remissionStore.receiptId();
|
||||
await this.#router.navigate(['../return-receipt', returnId, receiptId], {
|
||||
relativeTo: this.#route,
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.returnId();
|
||||
this.receiptId();
|
||||
this.#remissionStore.reloadReceipt();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,25 +19,17 @@ export class RemissionStartCardComponent {
|
||||
async startRemission() {
|
||||
const remissionStartDialogRef = this.#remissionStartDialog({
|
||||
data: { returnGroup: undefined },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(remissionStartDialogRef.closed);
|
||||
|
||||
if (result) {
|
||||
const {
|
||||
returnId,
|
||||
receiptId,
|
||||
receiptNumber,
|
||||
receiptItemsCount,
|
||||
packageNumber,
|
||||
} = result;
|
||||
const { returnId, receiptId } = result;
|
||||
this.#remissionStore.startRemission({
|
||||
returnId,
|
||||
receiptId,
|
||||
receiptNumber,
|
||||
receiptItemsCount,
|
||||
packageNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const createRemissionListResource = (
|
||||
return resource({
|
||||
params,
|
||||
loader: async ({ abortSignal, params }) => {
|
||||
console.log(params.queryToken);
|
||||
const assignedStock = await remissionStockService.fetchAssignedStock();
|
||||
|
||||
if (!assignedStock || !assignedStock.id) {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
@if (!returnResource.isLoading() && !returnResource.value()?.completed) {
|
||||
<ui-stateful-button
|
||||
class="fixed right-6 bottom-6"
|
||||
(click)="completeReturn()"
|
||||
(clicked)="completeReturn()"
|
||||
[(state)]="completeReturnState"
|
||||
defaultContent="Wanne abschließen"
|
||||
defaultWidth="13rem"
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
uiInputControl
|
||||
class="isa-text-body-2-bold placeholder:isa-text-body-2-bold"
|
||||
placeholder="Packstück ID scannen"
|
||||
type="number"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
[formControl]="control"
|
||||
(cleared)="control.setValue(undefined)"
|
||||
(blur)="control.updateValueAndValidity()"
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
:host {
|
||||
@apply flex flex-col gap-8;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-outer-spin-button,
|
||||
input[type="number"]::-webkit-inner-spin-button {
|
||||
@apply appearance-none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class CreateReturnReceiptComponent {
|
||||
ReturnReceiptResultType = ReturnReceiptResultType;
|
||||
createReturnReceipt = output<ReturnReceiptResult>();
|
||||
|
||||
control = new FormControl<number | undefined>(undefined, {
|
||||
control = new FormControl<string | undefined>(undefined, {
|
||||
validators: [Validators.required, Validators.pattern(/^\d{18}$/)],
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export class CreateReturnReceiptComponent {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
this.control.setValue(Number(value));
|
||||
this.control.setValue(value);
|
||||
}
|
||||
|
||||
onGenerate() {
|
||||
|
||||
@@ -10,11 +10,7 @@ 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 {
|
||||
Receipt,
|
||||
RemissionReturnReceiptService,
|
||||
Return,
|
||||
} from '@isa/remission/data-access';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
|
||||
export enum ReturnReceiptResultType {
|
||||
Close = 'close',
|
||||
@@ -27,7 +23,7 @@ export type ReturnReceiptResult =
|
||||
| { type: ReturnReceiptResultType.Generate }
|
||||
| {
|
||||
type: ReturnReceiptResultType.Input;
|
||||
value: number | undefined | null;
|
||||
value: string | undefined | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -38,9 +34,6 @@ export type RemissionStartDialogData = {
|
||||
export type RemissionStartDialogResult = {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
receiptNumber: string;
|
||||
receiptItemsCount: number;
|
||||
packageNumber: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -69,64 +62,46 @@ export class RemissionStartDialogComponent extends DialogContentDirective<
|
||||
|
||||
onAssignPackageNumber(packageNumber: string | undefined) {
|
||||
const returnReceipt = this.createReturnReceipt();
|
||||
if (packageNumber && returnReceipt) {
|
||||
this.startRemission({ returnReceipt, packageNumber });
|
||||
if (
|
||||
packageNumber &&
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async startRemission({
|
||||
returnReceipt,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}: {
|
||||
returnReceipt: ReturnReceiptResult;
|
||||
receiptNumber: string | undefined;
|
||||
packageNumber: string;
|
||||
}) {
|
||||
this.loadRequests.set(true);
|
||||
|
||||
// Warenbegleitschein erstellen
|
||||
const createdReturn: Return | undefined =
|
||||
await this.#remissionReturnReceiptService.createReturn({
|
||||
returnGroup: this.data.returnGroup,
|
||||
});
|
||||
|
||||
if (!createdReturn || !returnReceipt) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
let receiptNumber: string | undefined;
|
||||
if (returnReceipt.type === ReturnReceiptResultType.Input) {
|
||||
receiptNumber = String(returnReceipt.value);
|
||||
}
|
||||
|
||||
if (returnReceipt.type === ReturnReceiptResultType.Generate) {
|
||||
receiptNumber = undefined; // Wird generiert
|
||||
}
|
||||
|
||||
const createdReceipt: Receipt | undefined =
|
||||
await this.#remissionReturnReceiptService.createReceipt({
|
||||
returnId: createdReturn.id,
|
||||
receiptNumber,
|
||||
});
|
||||
|
||||
if (!createdReceipt) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
// Wannennummer zuweisen
|
||||
await this.#remissionReturnReceiptService.assignPackage({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
const response = await this.#remissionReturnReceiptService.startRemission({
|
||||
returnGroup: this.data.returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
this.onDialogClose({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
receiptNumber: createdReceipt.receiptNumber,
|
||||
receiptItemsCount: createdReceipt?.items?.length ?? 0,
|
||||
packageNumber,
|
||||
returnId: response.returnId,
|
||||
receiptId: response.receiptId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,4 @@
|
||||
@apply overflow-y-auto overflow-x-hidden;
|
||||
@apply min-h-0;
|
||||
}
|
||||
|
||||
&:has(ui-feedback-dialog),
|
||||
&:has(remi-remission-start-dialog) {
|
||||
@apply gap-0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { DialogComponent } from './dialog.component';
|
||||
import { DialogContentDirective } from './dialog-content.directive';
|
||||
import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
|
||||
import { DIALOG_CLASS_LIST, DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
|
||||
import { Component } from '@angular/core';
|
||||
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
|
||||
import { NgComponentOutlet } from '@angular/common';
|
||||
@@ -12,13 +12,18 @@ import { NgComponentOutlet } from '@angular/common';
|
||||
template: '<div>Mock Content</div>',
|
||||
standalone: true,
|
||||
})
|
||||
class MockDialogContentComponent extends DialogContentDirective<unknown, unknown> {}
|
||||
class MockDialogContentComponent extends DialogContentDirective<
|
||||
unknown,
|
||||
unknown
|
||||
> {}
|
||||
|
||||
describe('DialogComponent', () => {
|
||||
let spectator: Spectator<DialogComponent<unknown, unknown, MockDialogContentComponent>>;
|
||||
let spectator: Spectator<
|
||||
DialogComponent<unknown, unknown, MockDialogContentComponent>
|
||||
>;
|
||||
const mockTitle = 'Test Dialog Title';
|
||||
const mockDialogRef = { close: jest.fn() };
|
||||
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: DialogComponent,
|
||||
imports: [NgComponentOutlet, MockDialogContentComponent], // Use imports instead of declarations for standalone components
|
||||
@@ -26,7 +31,8 @@ describe('DialogComponent', () => {
|
||||
{ provide: DIALOG_TITLE, useValue: mockTitle },
|
||||
{ provide: DIALOG_CONTENT, useValue: MockDialogContentComponent },
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: {} }
|
||||
{ provide: DIALOG_DATA, useValue: {} },
|
||||
{ provide: DIALOG_CLASS_LIST, useValue: [] },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { DialogContentDirective } from './dialog-content.directive';
|
||||
import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
|
||||
import { DIALOG_CLASS_LIST, DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
|
||||
import { ComponentType } from '@angular/cdk/portal';
|
||||
import { NgComponentOutlet } from '@angular/common';
|
||||
|
||||
@@ -23,7 +24,7 @@ import { NgComponentOutlet } from '@angular/common';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgComponentOutlet],
|
||||
host: {
|
||||
'[class]': '["ui-dialog"]',
|
||||
'[class]': 'classes()',
|
||||
},
|
||||
})
|
||||
export class DialogComponent<D, R, C extends DialogContentDirective<D, R>> {
|
||||
@@ -32,4 +33,18 @@ export class DialogComponent<D, R, C extends DialogContentDirective<D, R>> {
|
||||
|
||||
/** The component type to instantiate as the dialog content */
|
||||
readonly component = inject(DIALOG_CONTENT) as ComponentType<C>;
|
||||
|
||||
/** Additional CSS classes provided via injection */
|
||||
private readonly classList =
|
||||
inject(DIALOG_CLASS_LIST, { optional: true }) ?? [];
|
||||
|
||||
/**
|
||||
* Computed property that combines the base dialog class with any additional classes
|
||||
* This allows for dynamic styling of the dialog based on external configuration
|
||||
*
|
||||
* @return An array of CSS class names to apply to the dialog element
|
||||
*/
|
||||
classes = computed(() => {
|
||||
return ['ui-dialog', ...this.classList];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
injectFeedbackDialog,
|
||||
injectMessageDialog,
|
||||
injectTextInputDialog,
|
||||
injectNumberInputDialog,
|
||||
} from './injects';
|
||||
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
|
||||
import { DialogComponent } from './dialog.component';
|
||||
@@ -13,6 +14,7 @@ import { Component } from '@angular/core';
|
||||
import { DialogContentDirective } from './dialog-content.directive';
|
||||
import { TextInputDialogComponent } from './text-input-dialog/text-input-dialog.component';
|
||||
import { FeedbackDialogComponent } from './feedback-dialog/feedback-dialog.component';
|
||||
import { NumberInputDialogComponent } from './number-input-dialog/number-input-dialog.component';
|
||||
|
||||
// Test component extending DialogContentDirective for testing
|
||||
@Component({ template: '' })
|
||||
@@ -177,6 +179,27 @@ describe('Dialog Injects', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectNumberInputDialog', () => {
|
||||
it('should create a dialog injector for NumberInputDialogComponent', () => {
|
||||
// Act
|
||||
const openNumberInputDialog = TestBed.runInInjectionContext(() =>
|
||||
injectNumberInputDialog(),
|
||||
);
|
||||
openNumberInputDialog({
|
||||
data: {
|
||||
message: 'Test message',
|
||||
inputLabel: 'Enter value',
|
||||
inputDefaultValue: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const callOptions = mockDialogOpen.mock.calls[0][1];
|
||||
const injector = callOptions.injector;
|
||||
expect(injector.get(DIALOG_CONTENT)).toBe(NumberInputDialogComponent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectFeedbackDialog', () => {
|
||||
it('should create a dialog injector for FeedbackDialogComponent', () => {
|
||||
// Act
|
||||
|
||||
@@ -4,9 +4,10 @@ import { ComponentType } from '@angular/cdk/portal';
|
||||
import { inject, Injector } from '@angular/core';
|
||||
import { DialogContentDirective } from './dialog-content.directive';
|
||||
import { DialogComponent } from './dialog.component';
|
||||
import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
|
||||
import { DIALOG_CLASS_LIST, DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
|
||||
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
|
||||
import { TextInputDialogComponent } from './text-input-dialog/text-input-dialog.component';
|
||||
import { NumberInputDialogComponent } from './number-input-dialog/number-input-dialog.component';
|
||||
import {
|
||||
FeedbackDialogComponent,
|
||||
FeedbackDialogData,
|
||||
@@ -16,6 +17,9 @@ export interface InjectDialogOptions {
|
||||
/** Optional title override for the dialog */
|
||||
title?: string;
|
||||
|
||||
/** Optional additional CSS classes to apply to the dialog */
|
||||
classList?: string[];
|
||||
|
||||
/** Optional width for the dialog */
|
||||
width?: string;
|
||||
|
||||
@@ -81,6 +85,10 @@ export function injectDialog<C extends DialogContentDirective<any, any>>(
|
||||
provide: DIALOG_TITLE,
|
||||
useValue: openOptions?.title ?? injectOptions?.title,
|
||||
},
|
||||
{
|
||||
provide: DIALOG_CLASS_LIST,
|
||||
useValue: openOptions?.classList ?? injectOptions?.classList ?? [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -119,6 +127,13 @@ export const injectMessageDialog = () => injectDialog(MessageDialogComponent);
|
||||
export const injectTextInputDialog = () =>
|
||||
injectDialog(TextInputDialogComponent);
|
||||
|
||||
/**
|
||||
* Convenience function that returns a pre-configured NumberInputDialog injector
|
||||
* @returns A function to open a number input dialog
|
||||
*/
|
||||
export const injectNumberInputDialog = () =>
|
||||
injectDialog(NumberInputDialogComponent);
|
||||
|
||||
/**
|
||||
* Convenience function that returns a pre-configured FeedbackDialog injector
|
||||
* @returns A function to open a feedback dialog
|
||||
@@ -129,5 +144,6 @@ export const injectFeedbackDialog = (
|
||||
injectDialog(FeedbackDialogComponent, {
|
||||
disableClose: false,
|
||||
minWidth: '20rem',
|
||||
classList: ['gap-0'],
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -16,3 +16,11 @@ export const DIALOG_TITLE = new InjectionToken<string | undefined>(
|
||||
export const DIALOG_CONTENT = new InjectionToken<ComponentType<unknown>>(
|
||||
'DIALOG_CONTENT',
|
||||
);
|
||||
|
||||
/**
|
||||
* Injection token for providing additional CSS classes to the dialog
|
||||
* Allows customization of dialog appearance through external styles
|
||||
*/
|
||||
export const DIALOG_CLASS_LIST = new InjectionToken<string[]>(
|
||||
'DIALOG_CLASS_LIST',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user