mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1913: feat(remission): refactor return receipt details and extract shared actions
feat(remission): refactor return receipt details and extract shared actions Refactor remission return receipt details to use return-based data flow instead of individual receipt fetching. Extract reusable action components for better code organization and consistency. - Remove deprecated fetchRemissionReturnReceipt method and schema - Add helper functions for extracting data from return objects - Replace receipt-specific components with return-based equivalents - Create shared return-receipt-actions library with reusable components - Update components to use modern Angular patterns (signals, computed) - Improve data flow consistency across remission features - Add comprehensive test coverage for new components - Update eager loading support in fetch return functionality The new architecture provides better data consistency and reduces code duplication by centralizing receipt actions and data extraction logic into reusable components. Refs: #5242, #5138, #5232, #5241
This commit is contained in:
committed by
Andreas Schickinger
parent
2e012a124a
commit
99e8e7cfe0
@@ -4,16 +4,14 @@ import { firstValueFrom } from 'rxjs';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { ReturnTaskListStore } from '@isa/oms/data-access';
|
||||
import { ReturnReviewComponent } from '../return-review.component';
|
||||
import { ConfirmationDialogComponent, injectDialog } from '@isa/ui/dialog';
|
||||
import { injectConfirmationDialog } from '@isa/ui/dialog';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UncompletedTasksGuard
|
||||
implements CanDeactivate<ReturnReviewComponent>
|
||||
{
|
||||
#returnTaskListStore = inject(ReturnTaskListStore);
|
||||
#confirmationDialog = injectDialog(ConfirmationDialogComponent, {
|
||||
title: 'Aufgaben erledigen',
|
||||
});
|
||||
#confirmationDialog = injectConfirmationDialog();
|
||||
|
||||
processId = injectTabId();
|
||||
|
||||
@@ -45,6 +43,7 @@ export class UncompletedTasksGuard
|
||||
|
||||
async openDialog(): Promise<boolean> {
|
||||
const confirmDialogRef = this.#confirmationDialog({
|
||||
title: 'Aufgaben erledigen',
|
||||
data: {
|
||||
message:
|
||||
'Bitte schließen Sie die Aufgaben ab bevor Sie das die Rückgabe verlassen',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Return } from '../models';
|
||||
|
||||
/**
|
||||
* Extracts all package numbers from all receipts in a return.
|
||||
* Only includes package numbers from receipts that have loaded data and where the package data exists.
|
||||
*
|
||||
* @param returnData - The return object containing receipts
|
||||
* @returns Comma-separated string of all package numbers from all receipts, or empty string if no packages found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const packageNumbers = getPackageNumbersFromReturn(returnData);
|
||||
* console.log(`Package numbers: ${packageNumbers}`); // "PKG-001, PKG-002, PKG-003"
|
||||
* ```
|
||||
*/
|
||||
export const getPackageNumbersFromReturn = (returnData: Return): string => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const allPackageNumbers = returnData.receipts.reduce<string[]>(
|
||||
(packageNumbers, receipt) => {
|
||||
const receiptPackages = receipt.data?.packages || [];
|
||||
|
||||
// Extract package numbers from loaded packages, filtering out packages without data or packageNumber
|
||||
const receiptPackageNumbers = receiptPackages
|
||||
.filter((pkg) => pkg.data?.packageNumber)
|
||||
.map((pkg) => pkg.data!.packageNumber!);
|
||||
|
||||
packageNumbers.push(...receiptPackageNumbers);
|
||||
return packageNumbers;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return allPackageNumbers.join(', ');
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Return } from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to calculate the total item quantity from all receipts in a return.
|
||||
* If no receipts are present, returns 0.
|
||||
* @param {Return} returnData - The return object containing receipts
|
||||
* @return {number} Total item quantity from all receipts
|
||||
*/
|
||||
export const getReceiptItemQuantityFromReturn = (
|
||||
returnData: Return,
|
||||
): number => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return returnData.receipts.reduce((totalItems, receipt) => {
|
||||
const items = receipt.data?.items;
|
||||
return totalItems + (items ? items.length : 0);
|
||||
}, 0);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Return } from '../models';
|
||||
import { ReceiptItem } from '../models';
|
||||
|
||||
/**
|
||||
* Extracts all receipt item data from all receipts in a return.
|
||||
* Only includes items from receipts that have loaded data and where the item data exists.
|
||||
*
|
||||
* @param returnData - The return object containing receipts
|
||||
* @returns Array of all receipt item data from all receipts, or empty array if no items found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const items = getReceiptItemsFromReturn(returnData);
|
||||
* console.log(`Found ${items.length} receipt items across all receipts`);
|
||||
* ```
|
||||
*/
|
||||
export const getReceiptItemsFromReturn = (
|
||||
returnData: Return,
|
||||
): ReceiptItem[] => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return returnData.receipts.reduce<ReceiptItem[]>((items, receipt) => {
|
||||
const receiptItems = receipt.data?.items || [];
|
||||
|
||||
// Extract only the actual ReceiptItem data, filtering out items without data
|
||||
const itemData = receiptItems
|
||||
.filter((item) => item.data !== undefined)
|
||||
.map((item) => item.data!);
|
||||
|
||||
items.push(...itemData);
|
||||
return items;
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Return } from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to extract and format receipt numbers from a return object.
|
||||
* Returns "Keine Belege vorhanden" if no receipts, otherwise returns formatted receipt numbers.
|
||||
*
|
||||
* @param {Return} returnData - The return object containing receipts
|
||||
* @returns {string} The formatted receipt numbers or message
|
||||
*/
|
||||
export const getReceiptNumberFromReturn = (returnData: Return): string => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return 'Keine Belege vorhanden';
|
||||
}
|
||||
|
||||
const receiptNumbers = returnData.receipts
|
||||
.map((receipt) => receipt.data?.receiptNumber)
|
||||
.filter((receiptNumber) => receiptNumber && receiptNumber.length >= 12)
|
||||
.map((receiptNumber) => receiptNumber!.substring(6, 12));
|
||||
|
||||
return receiptNumbers.length > 0 ? receiptNumbers.join(', ') : '';
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { getReceiptStatusFromReturn } from './get-receipt-status-from-return.helper';
|
||||
import { ReceiptCompleteStatus, Return } from '../models';
|
||||
|
||||
describe('getReceiptStatusFromReturn', () => {
|
||||
it('should return Offen when no receipts exist', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [] as any,
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Offen);
|
||||
});
|
||||
|
||||
it('should return Offen when receipts array is undefined', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Offen);
|
||||
});
|
||||
|
||||
it('should return Abgeschlossen when at least one receipt is completed', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [
|
||||
{ data: { completed: 'Offen' } },
|
||||
{ data: { completed: 'Abgeschlossen' } },
|
||||
],
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Abgeschlossen);
|
||||
});
|
||||
|
||||
it('should return Abgeschlossen when all receipts are incomplete', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [
|
||||
{ data: { completed: 'Abgeschlossen' } },
|
||||
{ data: { completed: 'Abgeschlossen' } },
|
||||
],
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Abgeschlossen);
|
||||
});
|
||||
|
||||
it('should return Offen when receipt data is undefined', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [{ data: undefined }, {}],
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Offen);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
ReceiptCompleteStatus,
|
||||
ReceiptCompleteStatusValue,
|
||||
Return,
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to determine the receipt status from a return object.
|
||||
* Returns 'Offen' if no receipts or all are incomplete, otherwise returns 'Abgeschlossen'.
|
||||
*
|
||||
* @param {Return} returnData - The return object containing receipts
|
||||
* @returns {ReceiptCompleteStatusValue} The completion status of the return
|
||||
*/
|
||||
export const getReceiptStatusFromReturn = (
|
||||
returnData: Return,
|
||||
): ReceiptCompleteStatusValue => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return ReceiptCompleteStatus.Offen;
|
||||
}
|
||||
|
||||
const hasCompletedReceipt = returnData.receipts.some(
|
||||
(receipt) => receipt.data?.completed,
|
||||
);
|
||||
|
||||
return hasCompletedReceipt
|
||||
? ReceiptCompleteStatus.Abgeschlossen
|
||||
: ReceiptCompleteStatus.Offen;
|
||||
};
|
||||
@@ -2,3 +2,8 @@ export * from './calc-available-stock.helper';
|
||||
export * from './calc-stock-to-remit.helper';
|
||||
export * from './calc-target-stock.helper';
|
||||
export * from './calc-capacity.helper';
|
||||
export * from './get-receipt-status-from-return.helper';
|
||||
export * from './get-receipt-item-quantity-from-return.helper';
|
||||
export * from './get-receipt-number-from-return.helper';
|
||||
export * from './get-receipt-items-from-return.helper';
|
||||
export * from './get-package-numbers-from-return.helper';
|
||||
|
||||
@@ -17,3 +17,4 @@ export * from './receipt-return-suggestion-tuple';
|
||||
export * from './value-tuple-sting-and-integer';
|
||||
export * from './create-remission';
|
||||
export * from './remission-item-source';
|
||||
export * from './receipt-complete-status';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ReceiptCompleteStatus = {
|
||||
Offen: 'Offen',
|
||||
Abgeschlossen: 'Abgeschlossen',
|
||||
} as const;
|
||||
|
||||
export type ReceiptCompleteStatusKey = keyof typeof ReceiptCompleteStatus;
|
||||
export type ReceiptCompleteStatusValue =
|
||||
(typeof ReceiptCompleteStatus)[ReceiptCompleteStatusKey];
|
||||
@@ -1,51 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schema for validating remission return receipt fetch parameters.
|
||||
* Ensures both receiptId and returnId are valid numbers.
|
||||
*
|
||||
* @constant
|
||||
* @type {z.ZodObject}
|
||||
*
|
||||
* @example
|
||||
* const params = FetchRemissionReturnReceiptSchema.parse({
|
||||
* receiptId: '123',
|
||||
* returnId: '456'
|
||||
* });
|
||||
* // Result: { receiptId: 123, returnId: 456 }
|
||||
*/
|
||||
export const FetchRemissionReturnReceiptSchema = z.object({
|
||||
/**
|
||||
* The receipt identifier - coerced to number for flexibility.
|
||||
*/
|
||||
receiptId: z.coerce.number(),
|
||||
|
||||
/**
|
||||
* The return identifier - coerced to number for flexibility.
|
||||
*/
|
||||
returnId: z.coerce.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Type representing the parsed output of FetchRemissionReturnReceiptSchema.
|
||||
* Contains validated and coerced receiptId and returnId as numbers.
|
||||
*
|
||||
* @typedef {Object} FetchRemissionReturnReceipt
|
||||
* @property {number} receiptId - The validated receipt identifier
|
||||
* @property {number} returnId - The validated return identifier
|
||||
*/
|
||||
export type FetchRemissionReturnReceipt = z.infer<
|
||||
typeof FetchRemissionReturnReceiptSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* Type representing the input parameters for FetchRemissionReturnReceiptSchema.
|
||||
* Accepts string or number values that can be coerced to numbers.
|
||||
*
|
||||
* @typedef {Object} FetchRemissionReturnParams
|
||||
* @property {string | number} receiptId - The receipt identifier (can be string or number)
|
||||
* @property {string | number} returnId - The return identifier (can be string or number)
|
||||
*/
|
||||
export type FetchRemissionReturnParams = z.input<
|
||||
typeof FetchRemissionReturnReceiptSchema
|
||||
>;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FetchReturnSchema = z.object({
|
||||
returnId: z.coerce.number(),
|
||||
eagerLoading: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export type FetchReturn = z.infer<typeof FetchReturnSchema>;
|
||||
|
||||
export type FetchReturnParams = z.input<typeof FetchReturnSchema>;
|
||||
@@ -4,8 +4,8 @@ export * from './assign-package.schema';
|
||||
export * from './create-receipt.schema';
|
||||
export * from './create-return.schema';
|
||||
export * from './fetch-query-settings.schema';
|
||||
export * from './fetch-remission-return-receipt.schema';
|
||||
export * from './fetch-remission-return-receipts.schema';
|
||||
export * from './fetch-stock-in-stock.schema';
|
||||
export * from './query-token.schema';
|
||||
export * from './fetch-required-capacity.schema';
|
||||
export * from './fetch-return.schema';
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
let service: RemissionReturnReceiptService;
|
||||
let mockReturnService: {
|
||||
ReturnQueryReturns: jest.Mock;
|
||||
ReturnGetReturnReceipt: jest.Mock;
|
||||
ReturnGetReturn: jest.Mock;
|
||||
ReturnCreateReturn: jest.Mock;
|
||||
ReturnCreateReceipt: jest.Mock;
|
||||
ReturnCreateAndAssignPackage: jest.Mock;
|
||||
@@ -37,8 +37,11 @@ describe('RemissionReturnReceiptService', () => {
|
||||
ReturnDeleteReturnItem: jest.Mock;
|
||||
ReturnFinalizeReceipt: jest.Mock;
|
||||
ReturnFinalizeReturn: jest.Mock;
|
||||
ReturnFinalizeReturnGroup: jest.Mock;
|
||||
ReturnAddReturnItem: jest.Mock;
|
||||
ReturnAddReturnSuggestion: jest.Mock;
|
||||
ReturnCancelReturn: jest.Mock;
|
||||
ReturnCancelReturnReceipt: jest.Mock;
|
||||
};
|
||||
let mockRemissionStockService: {
|
||||
fetchAssignedStock: jest.Mock;
|
||||
@@ -89,7 +92,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
beforeEach(() => {
|
||||
mockReturnService = {
|
||||
ReturnQueryReturns: jest.fn(),
|
||||
ReturnGetReturnReceipt: jest.fn(),
|
||||
ReturnGetReturn: jest.fn(),
|
||||
ReturnCreateReturn: jest.fn(),
|
||||
ReturnCreateReceipt: jest.fn(),
|
||||
ReturnCreateAndAssignPackage: jest.fn(),
|
||||
@@ -97,8 +100,11 @@ describe('RemissionReturnReceiptService', () => {
|
||||
ReturnDeleteReturnItem: jest.fn(),
|
||||
ReturnFinalizeReceipt: jest.fn(),
|
||||
ReturnFinalizeReturn: jest.fn(),
|
||||
ReturnFinalizeReturnGroup: jest.fn(),
|
||||
ReturnAddReturnItem: jest.fn(),
|
||||
ReturnAddReturnSuggestion: jest.fn(),
|
||||
ReturnCancelReturn: jest.fn(),
|
||||
ReturnCancelReturnReceipt: jest.fn(),
|
||||
};
|
||||
|
||||
mockRemissionStockService = {
|
||||
@@ -230,88 +236,116 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRemissionReturnReceipt', () => {
|
||||
const mockReceipt: Receipt = {
|
||||
id: 101,
|
||||
receiptNumber: 'REC-2024-001',
|
||||
completed: '2024-01-15T10:30:00.000Z',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt;
|
||||
describe('fetchReturn', () => {
|
||||
const mockReturn: Return = {
|
||||
id: 123,
|
||||
receipts: [
|
||||
{
|
||||
id: 101,
|
||||
data: {
|
||||
id: 101,
|
||||
receiptNumber: 'REC-2024-001',
|
||||
completed: '2024-01-15T10:30:00.000Z',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as unknown as Return;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReturnService.ReturnGetReturnReceipt = jest.fn();
|
||||
mockReturnService.ReturnGetReturn = jest.fn();
|
||||
});
|
||||
|
||||
it('should fetch return receipt successfully', async () => {
|
||||
mockReturnService.ReturnGetReturnReceipt.mockReturnValue(
|
||||
of({ result: mockReceipt, error: null }),
|
||||
it('should fetch return successfully', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnGetReturn.mockReturnValue(
|
||||
of({ result: mockReturn, error: null }),
|
||||
);
|
||||
|
||||
const params = { receiptId: 101, returnId: 1 };
|
||||
const result = await service.fetchRemissionReturnReceipt(params);
|
||||
const params = { returnId: 123 };
|
||||
|
||||
expect(result).toEqual(mockReceipt);
|
||||
expect(mockReturnService.ReturnGetReturnReceipt).toHaveBeenCalledWith({
|
||||
receiptId: 101,
|
||||
returnId: 1,
|
||||
// Act
|
||||
const result = await service.fetchReturn(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockReturn);
|
||||
expect(mockReturnService.ReturnGetReturn).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
eagerLoading: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle abort signal', async () => {
|
||||
// Arrange
|
||||
const abortController = new AbortController();
|
||||
mockReturnService.ReturnGetReturnReceipt.mockReturnValue(
|
||||
of({ result: mockReceipt, error: null }),
|
||||
mockReturnService.ReturnGetReturn.mockReturnValue(
|
||||
of({ result: mockReturn, error: null }),
|
||||
);
|
||||
|
||||
const params = { receiptId: 101, returnId: 1 };
|
||||
await service.fetchRemissionReturnReceipt(params, abortController.signal);
|
||||
const params = { returnId: 123 };
|
||||
|
||||
expect(mockReturnService.ReturnGetReturnReceipt).toHaveBeenCalled();
|
||||
// Act
|
||||
await service.fetchReturn(params, abortController.signal);
|
||||
|
||||
// Assert
|
||||
expect(mockReturnService.ReturnGetReturn).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
eagerLoading: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
// Arrange
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnGetReturnReceipt.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
mockReturnService.ReturnGetReturn.mockReturnValue(of(errorResponse));
|
||||
|
||||
const params = { receiptId: 101, returnId: 1 };
|
||||
await expect(service.fetchRemissionReturnReceipt(params)).rejects.toThrow(
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.fetchReturn(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when result is null', async () => {
|
||||
mockReturnService.ReturnGetReturnReceipt.mockReturnValue(
|
||||
// Arrange
|
||||
mockReturnService.ReturnGetReturn.mockReturnValue(
|
||||
of({ result: null, error: null }),
|
||||
);
|
||||
|
||||
const params = { receiptId: 101, returnId: 1 };
|
||||
const result = await service.fetchRemissionReturnReceipt(params);
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act
|
||||
const result = await service.fetchReturn(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return undefined when result is undefined', async () => {
|
||||
mockReturnService.ReturnGetReturnReceipt.mockReturnValue(
|
||||
of({ error: null }),
|
||||
);
|
||||
// Arrange
|
||||
mockReturnService.ReturnGetReturn.mockReturnValue(of({ error: null }));
|
||||
|
||||
const params = { receiptId: 101, returnId: 1 };
|
||||
const result = await service.fetchRemissionReturnReceipt(params);
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act
|
||||
const result = await service.fetchReturn(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
mockReturnService.ReturnGetReturnReceipt.mockReturnValue(
|
||||
// Arrange
|
||||
mockReturnService.ReturnGetReturn.mockReturnValue(
|
||||
throwError(() => new Error('Observable error')),
|
||||
);
|
||||
|
||||
const params = { receiptId: 101, returnId: 1 };
|
||||
await expect(service.fetchRemissionReturnReceipt(params)).rejects.toThrow(
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.fetchReturn(params)).rejects.toThrow(
|
||||
'Observable error',
|
||||
);
|
||||
});
|
||||
@@ -764,6 +798,74 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturnGroup', () => {
|
||||
const mockCompletedReturns: Return[] = [
|
||||
{
|
||||
id: 1,
|
||||
receipts: [
|
||||
{
|
||||
id: 101,
|
||||
data: {
|
||||
id: 101,
|
||||
receiptNumber: 'REC-2024-001',
|
||||
completed: '2024-01-15T10:30:00.000Z',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as unknown as Return,
|
||||
{
|
||||
id: 2,
|
||||
receipts: [
|
||||
{
|
||||
id: 102,
|
||||
data: {
|
||||
id: 102,
|
||||
receiptNumber: 'REC-2024-002',
|
||||
completed: '2024-01-15T11:30:00.000Z',
|
||||
created: '2024-01-15T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as unknown as Return,
|
||||
];
|
||||
|
||||
it('should complete return group successfully', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnFinalizeReturnGroup.mockReturnValue(
|
||||
of({ result: mockCompletedReturns, error: null }),
|
||||
);
|
||||
|
||||
const params = { returnGroup: 'group-123' };
|
||||
|
||||
// Act
|
||||
const result = await service.completeReturnGroup(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockCompletedReturns);
|
||||
expect(mockReturnService.ReturnFinalizeReturnGroup).toHaveBeenCalledWith(
|
||||
params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
// Arrange
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnFinalizeReturnGroup.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
|
||||
const params = { returnGroup: 'group-123' };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.completeReturnGroup(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturnReceiptAndReturn', () => {
|
||||
const mockCompletedReceipt: Receipt = {
|
||||
id: 101,
|
||||
@@ -1523,4 +1625,112 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('cancelReturn', () => {
|
||||
beforeEach(() => {
|
||||
mockReturnService.ReturnCancelReturn = jest.fn();
|
||||
});
|
||||
|
||||
it('should cancel return successfully', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnCancelReturn.mockReturnValue(
|
||||
of({ result: null, error: null }),
|
||||
);
|
||||
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act
|
||||
await service.cancelReturn(params);
|
||||
|
||||
// Assert
|
||||
expect(mockReturnService.ReturnCancelReturn).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
// Arrange
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnCancelReturn.mockReturnValue(of(errorResponse));
|
||||
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.cancelReturn(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnCancelReturn.mockReturnValue(
|
||||
throwError(() => new Error('Observable error')),
|
||||
);
|
||||
|
||||
const params = { returnId: 123 };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.cancelReturn(params)).rejects.toThrow(
|
||||
'Observable error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelReturnReceipt', () => {
|
||||
beforeEach(() => {
|
||||
mockReturnService.ReturnCancelReturnReceipt = jest.fn();
|
||||
});
|
||||
|
||||
it('should cancel return receipt successfully', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnCancelReturnReceipt.mockReturnValue(
|
||||
of({ result: null, error: null }),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
};
|
||||
|
||||
// Act
|
||||
await service.cancelReturnReceipt(params);
|
||||
|
||||
// Assert
|
||||
expect(mockReturnService.ReturnCancelReturnReceipt).toHaveBeenCalledWith(
|
||||
params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
// Arrange
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnCancelReturnReceipt.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.cancelReturnReceipt(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnCancelReturnReceipt.mockReturnValue(
|
||||
throwError(() => new Error('Observable error')),
|
||||
);
|
||||
|
||||
const params = {
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.cancelReturnReceipt(params)).rejects.toThrow(
|
||||
'Observable error',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
CreateReceipt,
|
||||
CreateReturn,
|
||||
CreateReturnSchema,
|
||||
FetchRemissionReturnParams,
|
||||
FetchRemissionReturnReceiptSchema,
|
||||
FetchRemissionReturnReceiptsParams,
|
||||
FetchRemissionReturnReceiptsSchema,
|
||||
FetchReturnParams,
|
||||
FetchReturnSchema,
|
||||
} from '../schemas';
|
||||
import {
|
||||
CreateRemission,
|
||||
@@ -126,43 +126,97 @@ export class RemissionReturnReceiptService {
|
||||
return returns;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Fetches a specific remission return receipt by receipt and return IDs.
|
||||
// * Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
// *
|
||||
// * @async
|
||||
// * @param {FetchRemissionReturnParams} params - The receipt and return identifiers
|
||||
// * @param {FetchRemissionReturnParams} params.receiptId - ID of the receipt to fetch
|
||||
// * @param {FetchRemissionReturnParams} params.returnId - ID of the return containing the receipt
|
||||
// * @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
// * @returns {Promise<Receipt | undefined>} The receipt object if found, undefined otherwise
|
||||
// * @throws {ResponseArgsError} When the API request fails
|
||||
// * @throws {z.ZodError} When parameter validation fails
|
||||
// *
|
||||
// * @example
|
||||
// * const receipt = await service.fetchRemissionReturnReceipt({
|
||||
// * receiptId: '123',
|
||||
// * returnId: '456'
|
||||
// * });
|
||||
// */
|
||||
// async fetchRemissionReturnReceipt(
|
||||
// params: FetchRemissionReturnParams,
|
||||
// abortSignal?: AbortSignal,
|
||||
// ): Promise<Receipt | undefined> {
|
||||
// this.#logger.debug('Fetching remission return receipt', () => ({ params }));
|
||||
|
||||
// const { receiptId, returnId } =
|
||||
// FetchRemissionReturnReceiptSchema.parse(params);
|
||||
|
||||
// this.#logger.info('Fetching return receipt from API', () => ({
|
||||
// receiptId,
|
||||
// returnId,
|
||||
// }));
|
||||
|
||||
// let req$ = this.#returnService.ReturnGetReturnReceipt({
|
||||
// receiptId,
|
||||
// returnId,
|
||||
// eagerLoading: 2,
|
||||
// });
|
||||
|
||||
// if (abortSignal) {
|
||||
// this.#logger.debug('Request configured with abort signal');
|
||||
// req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
// }
|
||||
|
||||
// const res = await firstValueFrom(req$);
|
||||
|
||||
// if (res?.error) {
|
||||
// this.#logger.error(
|
||||
// 'Failed to fetch return receipt',
|
||||
// new Error(res.message || 'Unknown error'),
|
||||
// );
|
||||
// throw new ResponseArgsError(res);
|
||||
// }
|
||||
|
||||
// const receipt = res?.result as Receipt | undefined;
|
||||
// this.#logger.debug('Successfully fetched return receipt', () => ({
|
||||
// found: !!receipt,
|
||||
// }));
|
||||
|
||||
// return receipt;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Fetches a specific remission return receipt by receipt and return IDs.
|
||||
* Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
* Fetches a remission return by its ID.
|
||||
* Validates parameters using FetchReturnSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {FetchRemissionReturnParams} params - The receipt and return identifiers
|
||||
* @param {FetchRemissionReturnParams} params.receiptId - ID of the receipt to fetch
|
||||
* @param {FetchRemissionReturnParams} params.returnId - ID of the return containing the receipt
|
||||
* @param {FetchReturnParams} params - The parameters for fetching the return
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Receipt | undefined>} The receipt object if found, undefined otherwise
|
||||
* @returns {Promise<Return | undefined>} The return object if found, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const receipt = await service.fetchRemissionReturnReceipt({
|
||||
* receiptId: '123',
|
||||
* returnId: '456'
|
||||
* });
|
||||
* const returnData = await service.fetchReturn({ returnId: 123 });
|
||||
*/
|
||||
async fetchRemissionReturnReceipt(
|
||||
params: FetchRemissionReturnParams,
|
||||
async fetchReturn(
|
||||
params: FetchReturnParams,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
this.#logger.debug('Fetching remission return receipt', () => ({ params }));
|
||||
): Promise<Return | undefined> {
|
||||
this.#logger.debug('Fetching remission return', () => ({ params }));
|
||||
|
||||
const { receiptId, returnId } =
|
||||
FetchRemissionReturnReceiptSchema.parse(params);
|
||||
const { returnId, eagerLoading = 2 } = FetchReturnSchema.parse(params);
|
||||
|
||||
this.#logger.info('Fetching return receipt from API', () => ({
|
||||
receiptId,
|
||||
this.#logger.info('Fetching return from API', () => ({
|
||||
returnId,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnGetReturnReceipt({
|
||||
receiptId,
|
||||
let req$ = this.#returnService.ReturnGetReturn({
|
||||
returnId,
|
||||
eagerLoading: 2,
|
||||
eagerLoading,
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
@@ -174,18 +228,18 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch return receipt',
|
||||
'Failed to fetch return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
this.#logger.debug('Successfully fetched return receipt', () => ({
|
||||
found: !!receipt,
|
||||
const returnData = res?.result as Return | undefined;
|
||||
this.#logger.debug('Successfully fetched return', () => ({
|
||||
found: !!returnData,
|
||||
}));
|
||||
|
||||
return receipt;
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,6 +471,56 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a return receipt and the associated return.
|
||||
* Validates parameters before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for the cancellation
|
||||
* @param {number} params.returnId - ID of the return to cancel
|
||||
* @param {number} params.receiptId - ID of the receipt to cancel
|
||||
* @return {Promise<void>} Resolves when the cancellation is successful
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*/
|
||||
async cancelReturnReceipt(params: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}): Promise<void> {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnCancelReturnReceipt(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to cancel return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a single return receipt and the associated return.
|
||||
* Validates parameters before making the request.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Return>} The completed return object
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*/
|
||||
async cancelReturn(params: { returnId: number }): Promise<void> {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnCancelReturn(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to cancel return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteReturnItem(params: { itemId: number }) {
|
||||
this.#logger.debug('Deleting return item', () => ({ params }));
|
||||
const res = await firstValueFrom(
|
||||
@@ -485,6 +589,30 @@ export class RemissionReturnReceiptService {
|
||||
return res?.result as Return;
|
||||
}
|
||||
|
||||
async completeReturnGroup(params: { returnGroup: string }) {
|
||||
this.#logger.debug('Completing return group', () => ({
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReturnGroup(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return group',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
this.#logger.info('Successfully completed return group', () => ({
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
return res?.result as Return[];
|
||||
}
|
||||
|
||||
async completeReturnReceiptAndReturn(params: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('RemissionStore', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const mockRemissionReturnReceiptService = {
|
||||
fetchRemissionReturnReceipt: jest.fn(),
|
||||
fetchReturn: jest.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
@@ -72,52 +72,29 @@ export const RemissionStore = signalStore(
|
||||
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
|
||||
* Resource for fetching the receipt data based on the current receiptId.
|
||||
* This resource is automatically reloaded when the receiptId changes.
|
||||
* @returnId is undefined, the resource will not fetch any data.
|
||||
* @returnId is set, it fetches the receipt data from the service.
|
||||
*/
|
||||
_receiptResource: resource({
|
||||
_fetchReturnResource: resource({
|
||||
params: () => ({
|
||||
returnId: store.returnId(),
|
||||
receiptId: store.receiptId(),
|
||||
}),
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
const { receiptId, returnId } = params;
|
||||
const { returnId } = params;
|
||||
|
||||
if (!receiptId || !returnId) {
|
||||
if (!returnId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const receipt =
|
||||
await remissionReturnReceiptService.fetchRemissionReturnReceipt(
|
||||
{
|
||||
returnId,
|
||||
receiptId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
return receipt;
|
||||
const returnData = await remissionReturnReceiptService.fetchReturn(
|
||||
{
|
||||
returnId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
return returnData;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -126,7 +103,7 @@ export const RemissionStore = signalStore(
|
||||
remissionStarted: computed(
|
||||
() => store.returnId() !== undefined && store.receiptId() !== undefined,
|
||||
),
|
||||
receipt: computed(() => store._receiptResource.value()),
|
||||
returnData: computed(() => store._fetchReturnResource.value()),
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
/**
|
||||
@@ -158,15 +135,44 @@ export const RemissionStore = signalStore(
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
store._receiptResource.reload();
|
||||
store._fetchReturnResource.reload();
|
||||
store.storeState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reloads the receipt resource.
|
||||
* This method should be called when the receipt data needs to be refreshed.
|
||||
* Reloads the return resource to fetch the latest data.
|
||||
* This is useful when the return data might have changed and needs to be refreshed.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* remissionStore.reloadReturn();
|
||||
* ```
|
||||
*/
|
||||
reloadReceipt() {
|
||||
store._receiptResource.reload();
|
||||
reloadReturn() {
|
||||
store._fetchReturnResource.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the current remission matches the provided returnId and receiptId.
|
||||
* This is useful for determining if the current remission is active in the context of a component.
|
||||
*
|
||||
* @param returnId - The return ID to check against the current remission
|
||||
* @param receiptId - The receipt ID to check against the current remission
|
||||
* @returns {boolean} True if the current remission matches the provided IDs, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isCurrent = remissionStore.isCurrentRemission(123, 456);
|
||||
* ```
|
||||
*/
|
||||
isCurrentRemission({
|
||||
returnId,
|
||||
receiptId,
|
||||
}: {
|
||||
returnId: number | undefined;
|
||||
receiptId: number | undefined;
|
||||
}): boolean {
|
||||
return store.returnId() === returnId && store.receiptId() === receiptId;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -273,15 +279,15 @@ export const RemissionStore = signalStore(
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the remission store to its initial state.
|
||||
* Clears all selected items, quantities, and resets return/receipt IDs.
|
||||
* Clears the remission store state, resetting all values to their initial state.
|
||||
* This is useful for starting a new remission process or clearing the current state.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* remissionStore.resetRemission();
|
||||
* remissionStore.clearState();
|
||||
* ```
|
||||
*/
|
||||
finishRemission() {
|
||||
clearState() {
|
||||
patchState(store, initialState);
|
||||
store.storeState();
|
||||
},
|
||||
|
||||
@@ -333,7 +333,7 @@ export class RemissionListComponent {
|
||||
onDeleteRemissionListItem(inProgress: boolean) {
|
||||
this.deleteRemissionListItemInProgress.set(inProgress);
|
||||
if (!inProgress) {
|
||||
this.reloadListAndReceipt();
|
||||
this.reloadListAndReturnData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ export class RemissionListComponent {
|
||||
}
|
||||
await this.remitItems();
|
||||
}
|
||||
this.reloadListAndReceipt();
|
||||
this.reloadListAndReturnData();
|
||||
this.searchTrigger.set('reload');
|
||||
}
|
||||
});
|
||||
@@ -435,7 +435,7 @@ export class RemissionListComponent {
|
||||
}
|
||||
|
||||
this.remitItemsState.set('success');
|
||||
this.reloadListAndReceipt();
|
||||
this.reloadListAndReturnData();
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remit items', error);
|
||||
this.remitItemsError.set(
|
||||
@@ -451,11 +451,11 @@ export class RemissionListComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the remission list and receipt data.
|
||||
* Reloads the remission list and return data.
|
||||
* This method is used to refresh the displayed data after changes.
|
||||
*/
|
||||
reloadListAndReceipt() {
|
||||
reloadListAndReturnData() {
|
||||
this.remissionResource.reload();
|
||||
this.#store.reloadReceipt();
|
||||
this.#store.reloadReturn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { RemissionStore } from '@isa/remission/data-access';
|
||||
import {
|
||||
getReceiptItemQuantityFromReturn,
|
||||
getReceiptNumberFromReturn,
|
||||
RemissionStore,
|
||||
} from '@isa/remission/data-access';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
@Component({
|
||||
@@ -24,20 +28,20 @@ export class RemissionReturnCardComponent {
|
||||
receiptId = computed(() => this.#remissionStore.receiptId());
|
||||
|
||||
receiptItemsCount = computed(() => {
|
||||
const receipt = this.#remissionStore.receipt();
|
||||
return receipt?.items?.length ?? 0;
|
||||
const returnData = this.#remissionStore.returnData();
|
||||
return getReceiptItemQuantityFromReturn(returnData!);
|
||||
});
|
||||
|
||||
receiptNumber = computed(() => {
|
||||
const receipt = this.#remissionStore.receipt();
|
||||
return receipt?.receiptNumber?.substring(6, 12);
|
||||
const returnData = this.#remissionStore.returnData();
|
||||
return getReceiptNumberFromReturn(returnData!);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.returnId();
|
||||
this.receiptId();
|
||||
this.#remissionStore.reloadReceipt();
|
||||
this.#remissionStore.reloadReturn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { RemissionStore } from '@isa/remission/data-access';
|
||||
import { RemissionStartDialogComponent } from '@isa/remission/shared/remission-start-dialog';
|
||||
import { RemissionStartService } from '@isa/remission/shared/remission-start-dialog';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectDialog } from '@isa/ui/dialog';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-start-card',
|
||||
@@ -13,24 +10,9 @@ import { firstValueFrom } from 'rxjs';
|
||||
imports: [ButtonComponent],
|
||||
})
|
||||
export class RemissionStartCardComponent {
|
||||
#remissionStartDialog = injectDialog(RemissionStartDialogComponent);
|
||||
#remissionStore = inject(RemissionStore);
|
||||
#remissionStartService = inject(RemissionStartService);
|
||||
|
||||
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 } = result;
|
||||
this.#remissionStore.startRemission({
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
}
|
||||
await this.#remissionStartService.startRemission(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<ui-stateful-button
|
||||
[(state)]="state"
|
||||
defaultContent="Remittieren"
|
||||
successContent="Hinzugefügt"
|
||||
errorContent="Konnte nicht hinzugefügt werden."
|
||||
errorAction="Noch mal versuchen"
|
||||
defaultWidth="10rem"
|
||||
successWidth="20.375rem"
|
||||
errorWidth="32rem"
|
||||
[pending]="isLoading()"
|
||||
color="brand"
|
||||
size="large"
|
||||
class="remit-button"
|
||||
(clicked)="clickHandler()"
|
||||
(action)="retryHandler()"
|
||||
/>
|
||||
@@ -1 +0,0 @@
|
||||
// Component now uses ui-stateful-button which handles all styling
|
||||
@@ -1,58 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
signal,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { StatefulButtonComponent, StatefulButtonState } from '@isa/ui/buttons';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-remit-button',
|
||||
templateUrl: './remit-button.component.html',
|
||||
styleUrls: ['./remit-button.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [StatefulButtonComponent],
|
||||
})
|
||||
export class RemitButtonComponent implements OnDestroy {
|
||||
state = signal<StatefulButtonState>('default');
|
||||
isLoading = signal<boolean>(false);
|
||||
|
||||
private timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
clickHandler() {
|
||||
// Clear any existing timer to prevent multiple clicks from stacking
|
||||
this.clearTimer();
|
||||
|
||||
this.isLoading.set(true);
|
||||
this.timer = setTimeout(() => {
|
||||
this.isLoading.set(false);
|
||||
// Simulate an async operation, e.g., API call
|
||||
const success = Math.random() > 0.5; // Randomly succeed or fail
|
||||
if (success) {
|
||||
this.state.set('success');
|
||||
} else {
|
||||
this.state.set('error');
|
||||
}
|
||||
}, 100); // Simulate async operation
|
||||
}
|
||||
|
||||
retryHandler() {
|
||||
this.isLoading.set(true);
|
||||
this.timer = setTimeout(() => {
|
||||
this.isLoading.set(false);
|
||||
this.state.set('success');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private clearTimer(): void {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
class="isa-text-body-1-bold"
|
||||
*uiSkeletonLoader="loading(); height: '1.5rem'"
|
||||
>
|
||||
{{ positionCount() }}
|
||||
{{ itemQuantity() }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { signal } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { RemissionReturnReceiptDetailsCardComponent } from './remission-return-receipt-details-card.component';
|
||||
import { Receipt, Supplier } from '@isa/remission/data-access';
|
||||
|
||||
// Mock the supplier resource
|
||||
vi.mock('./resources', () => ({
|
||||
createSupplierResource: vi.fn(() => ({
|
||||
value: signal([]),
|
||||
isLoading: signal(false),
|
||||
error: signal(null),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('RemissionReturnReceiptDetailsCardComponent', () => {
|
||||
let component: RemissionReturnReceiptDetailsCardComponent;
|
||||
let fixture: ComponentFixture<RemissionReturnReceiptDetailsCardComponent>;
|
||||
|
||||
const mockSuppliers: Supplier[] = [
|
||||
{
|
||||
id: 123,
|
||||
name: 'Test Supplier GmbH',
|
||||
address: 'Test Street 1',
|
||||
} as Supplier,
|
||||
{
|
||||
id: 456,
|
||||
name: 'Another Supplier Ltd',
|
||||
address: 'Another Street 2',
|
||||
} as Supplier,
|
||||
];
|
||||
|
||||
const mockReceipt: Receipt = {
|
||||
id: 789,
|
||||
receiptNumber: 'RR-2024-001234-ABC',
|
||||
completed: true,
|
||||
created: new Date('2024-01-15T10:30:00Z'),
|
||||
supplier: {
|
||||
id: 123,
|
||||
name: 'Test Supplier GmbH',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
quantity: 5,
|
||||
product: { id: 1, name: 'Product 1' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
quantity: 3,
|
||||
product: { id: 2, name: 'Product 2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
packages: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
packageNumber: 'PKG-001',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
packageNumber: 'PKG-002',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as Receipt;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RemissionReturnReceiptDetailsCardComponent, DatePipe],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemissionReturnReceiptDetailsCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have default loading state', () => {
|
||||
expect(component.loading()).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept receipt input', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
expect(component.receipt()).toEqual(mockReceipt);
|
||||
});
|
||||
|
||||
it('should accept loading input', () => {
|
||||
fixture.componentRef.setInput('loading', false);
|
||||
expect(component.loading()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('status computed signal', () => {
|
||||
it('should return "Abgeschlossen" when receipt is completed', () => {
|
||||
const completedReceipt = { ...mockReceipt, completed: true };
|
||||
fixture.componentRef.setInput('receipt', completedReceipt);
|
||||
|
||||
expect(component.status()).toBe('Abgeschlossen');
|
||||
});
|
||||
|
||||
it('should return "Offen" when receipt is not completed', () => {
|
||||
const openReceipt = { ...mockReceipt, completed: false };
|
||||
fixture.componentRef.setInput('receipt', openReceipt);
|
||||
|
||||
expect(component.status()).toBe('Offen');
|
||||
});
|
||||
|
||||
it('should return "Offen" when no receipt provided', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
|
||||
expect(component.status()).toBe('Offen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('positionCount computed signal', () => {
|
||||
it('should return the number of items', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
|
||||
// mockReceipt has 2 items
|
||||
expect(component.positionCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 when no items', () => {
|
||||
const receiptWithoutItems = { ...mockReceipt, items: [] };
|
||||
fixture.componentRef.setInput('receipt', receiptWithoutItems);
|
||||
|
||||
expect(component.positionCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return undefined when no receipt provided', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
|
||||
expect(component.positionCount()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should count all items regardless of data', () => {
|
||||
const receiptWithUndefinedItems = {
|
||||
...mockReceipt,
|
||||
items: [
|
||||
{ id: 1, data: undefined },
|
||||
{ id: 2, data: { id: 2, quantity: 5 } },
|
||||
],
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', receiptWithUndefinedItems);
|
||||
|
||||
expect(component.positionCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supplier computed signal', () => {
|
||||
it('should return supplier name when found', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
(component.supplierResource as any).value = signal(mockSuppliers);
|
||||
|
||||
expect(component.supplier()).toBe('Test Supplier GmbH');
|
||||
});
|
||||
|
||||
it('should return "Unbekannt" when supplier not found', () => {
|
||||
const receiptWithUnknownSupplier = {
|
||||
...mockReceipt,
|
||||
supplier: { id: 999, name: 'Unknown' },
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', receiptWithUnknownSupplier);
|
||||
(component.supplierResource as any).value = signal(mockSuppliers);
|
||||
|
||||
expect(component.supplier()).toBe('Unbekannt');
|
||||
});
|
||||
|
||||
it('should return "Unbekannt" when no suppliers loaded', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
(component.supplierResource as any).value = signal([]);
|
||||
|
||||
expect(component.supplier()).toBe('Unbekannt');
|
||||
});
|
||||
|
||||
it('should return "Unbekannt" when no receipt provided', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
(component.supplierResource as any).value = signal(mockSuppliers);
|
||||
|
||||
expect(component.supplier()).toBe('Unbekannt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('completedAt computed signal', () => {
|
||||
it('should return created date', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
|
||||
expect(component.completedAt()).toEqual(mockReceipt.created);
|
||||
});
|
||||
|
||||
it('should return undefined when no receipt', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
|
||||
expect(component.completedAt()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remiDate computed signal', () => {
|
||||
it('should return completed date when available', () => {
|
||||
const completedDate = new Date('2024-01-20T15:45:00Z');
|
||||
const receiptWithCompleted = {
|
||||
...mockReceipt,
|
||||
completed: completedDate,
|
||||
created: new Date('2024-01-15T10:30:00Z'),
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', receiptWithCompleted);
|
||||
|
||||
expect(component.remiDate()).toEqual(completedDate);
|
||||
});
|
||||
|
||||
it('should return created date when completed date not available', () => {
|
||||
const receiptWithoutCompleted = {
|
||||
...mockReceipt,
|
||||
completed: false,
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', receiptWithoutCompleted);
|
||||
|
||||
expect(component.remiDate()).toEqual(mockReceipt.created);
|
||||
});
|
||||
|
||||
it('should return undefined when no receipt', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
|
||||
expect(component.remiDate()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('packageNumber computed signal', () => {
|
||||
it('should return comma-separated package numbers', () => {
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
|
||||
expect(component.packageNumber()).toBe('PKG-001, PKG-002');
|
||||
});
|
||||
|
||||
it('should return empty string when no packages', () => {
|
||||
const receiptWithoutPackages = { ...mockReceipt, packages: [] };
|
||||
fixture.componentRef.setInput('receipt', receiptWithoutPackages);
|
||||
|
||||
expect(component.packageNumber()).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when no receipt', () => {
|
||||
fixture.componentRef.setInput('receipt', undefined);
|
||||
|
||||
expect(component.packageNumber()).toBe('');
|
||||
});
|
||||
|
||||
it('should handle packages with undefined data', () => {
|
||||
const receiptWithUndefinedPackages = {
|
||||
...mockReceipt,
|
||||
packages: [
|
||||
{ id: 1, data: undefined },
|
||||
{ id: 2, data: { id: 2, packageNumber: 'PKG-002' } },
|
||||
],
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', receiptWithUndefinedPackages);
|
||||
|
||||
// packageNumber maps undefined values, which join as ', PKG-002'
|
||||
expect(component.packageNumber()).toBe(', PKG-002');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component reactivity', () => {
|
||||
it('should update computed signals when receipt changes', () => {
|
||||
// Initial receipt
|
||||
fixture.componentRef.setInput('receipt', mockReceipt);
|
||||
(component.supplierResource as any).value = signal(mockSuppliers);
|
||||
|
||||
expect(component.status()).toBe('Abgeschlossen');
|
||||
expect(component.positionCount()).toBe(2);
|
||||
|
||||
// Change receipt
|
||||
const newReceipt = {
|
||||
...mockReceipt,
|
||||
completed: false,
|
||||
items: [{ id: 1, data: { id: 1, quantity: 10 } }],
|
||||
};
|
||||
fixture.componentRef.setInput('receipt', newReceipt);
|
||||
|
||||
expect(component.status()).toBe('Offen');
|
||||
expect(component.positionCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should create supplier resource on initialization', () => {
|
||||
expect(component.supplierResource).toBeDefined();
|
||||
expect(component.supplierResource.value).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,25 +4,19 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
linkedSignal,
|
||||
} from '@angular/core';
|
||||
import { Receipt } from '@isa/remission/data-access';
|
||||
import {
|
||||
getPackageNumbersFromReturn,
|
||||
getReceiptItemQuantityFromReturn,
|
||||
getReceiptNumberFromReturn,
|
||||
getReceiptStatusFromReturn,
|
||||
ReceiptCompleteStatusValue,
|
||||
Return,
|
||||
} from '@isa/remission/data-access';
|
||||
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
|
||||
import { createSupplierResource } from './resources';
|
||||
|
||||
/**
|
||||
* Component that displays detailed information about a remission return receipt in a card format.
|
||||
* Shows supplier information, status, dates, item counts, and package numbers.
|
||||
*
|
||||
* @component
|
||||
* @selector remi-remission-return-receipt-details-card
|
||||
* @standalone
|
||||
*
|
||||
* @example
|
||||
* <remi-remission-return-receipt-details-card
|
||||
* [receipt]="receiptData"
|
||||
* [loading]="isLoading">
|
||||
* </remi-remission-return-receipt-details-card>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-remission-return-receipt-details-card',
|
||||
templateUrl: './remission-return-receipt-details-card.component.html',
|
||||
@@ -33,10 +27,10 @@ import { createSupplierResource } from './resources';
|
||||
})
|
||||
export class RemissionReturnReceiptDetailsCardComponent {
|
||||
/**
|
||||
* Input for the receipt data to display.
|
||||
* Input for the return data to be displayed in the card.
|
||||
* @input
|
||||
*/
|
||||
receipt = input<Receipt>();
|
||||
return = input.required<Return>();
|
||||
|
||||
/**
|
||||
* Input to control the loading state of the card.
|
||||
@@ -50,63 +44,57 @@ export class RemissionReturnReceiptDetailsCardComponent {
|
||||
*/
|
||||
supplierResource = createSupplierResource();
|
||||
|
||||
/**
|
||||
* Computed signal that determines the receipt status text.
|
||||
* @returns {'Abgeschlossen' | 'Offen'} Status text based on completion state
|
||||
*/
|
||||
status = computed(() => {
|
||||
return this.receipt()?.completed ? 'Abgeschlossen' : 'Offen';
|
||||
firstReceipt = computed(() => {
|
||||
const returnData = this.return();
|
||||
return returnData?.receipts?.[0]?.data;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that calculates the items in the receipt.
|
||||
* @returns {number} Count of items in the receipt or 0 if not available
|
||||
* Computed signal that retrieves the receipt number from the return data.
|
||||
* Uses the helper function to get the receipt number.
|
||||
* @returns {string} The receipt number from the return
|
||||
*/
|
||||
positionCount = computed(() => {
|
||||
const receipt = this.receipt();
|
||||
return receipt?.items?.length;
|
||||
receiptNumber = computed(() => {
|
||||
const returnData = this.return();
|
||||
return getReceiptNumberFromReturn(returnData);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that finds and returns the supplier name.
|
||||
* @returns {string} Supplier name or 'Unbekannt' if not found
|
||||
* Computed signal that calculates the total item quantity from all receipts in the return.
|
||||
* Uses the helper function to get the quantity.
|
||||
* @returns {number} The total item quantity from all receipts
|
||||
*/
|
||||
itemQuantity = computed(() => {
|
||||
const returnData = this.return();
|
||||
return getReceiptItemQuantityFromReturn(returnData);
|
||||
});
|
||||
|
||||
/**
|
||||
* Linked signal that determines the completion status of the return.
|
||||
* Uses the helper function to get the status based on the return data.
|
||||
* @returns {ReceiptCompleteStatusValue} The completion status of the return
|
||||
*/
|
||||
status = linkedSignal<ReceiptCompleteStatusValue>(() => {
|
||||
const returnData = this.return();
|
||||
return getReceiptStatusFromReturn(returnData);
|
||||
});
|
||||
|
||||
remiDate = computed(() => {
|
||||
const returnData = this.return();
|
||||
return returnData?.completed || returnData?.created;
|
||||
});
|
||||
|
||||
packageNumber = computed(() => {
|
||||
const returnData = this.return();
|
||||
return getPackageNumbersFromReturn(returnData);
|
||||
});
|
||||
|
||||
supplier = computed(() => {
|
||||
const receipt = this.receipt();
|
||||
const receipt = this.firstReceipt();
|
||||
const supplier = this.supplierResource.value();
|
||||
|
||||
return (
|
||||
supplier?.find((s) => s.id === receipt?.supplier?.id)?.name || 'Unbekannt'
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal for the receipt completion date.
|
||||
* @returns {Date | undefined} The creation date of the receipt
|
||||
*/
|
||||
completedAt = computed(() => {
|
||||
const receipt = this.receipt();
|
||||
return receipt?.created;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal for the remission date.
|
||||
* Prioritizes completed date over created date.
|
||||
* @returns {Date | undefined} The remission date
|
||||
*/
|
||||
remiDate = computed(() => {
|
||||
const receipt = this.receipt();
|
||||
return receipt?.completed || receipt?.created;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that concatenates all package numbers.
|
||||
* @returns {string} Comma-separated list of package numbers
|
||||
*/
|
||||
packageNumber = computed(() => {
|
||||
const receipt = this.receipt();
|
||||
return (
|
||||
receipt?.packages?.map((p) => p.data?.packageNumber).join(', ') || ''
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,492 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MockComponent, MockDirective } from 'ng-mocks';
|
||||
import { RemissionReturnReceiptDetailsItemComponent } from './remission-return-receipt-details-item.component';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
import {
|
||||
ReceiptItem,
|
||||
RemissionProductGroupService,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
describe('RemissionReturnReceiptDetailsItemComponent', () => {
|
||||
let component: RemissionReturnReceiptDetailsItemComponent;
|
||||
let fixture: ComponentFixture<RemissionReturnReceiptDetailsItemComponent>;
|
||||
|
||||
const mockReceiptItem: ReceiptItem = {
|
||||
id: 1,
|
||||
quantity: 5,
|
||||
product: {
|
||||
id: 123,
|
||||
name: 'Test Product',
|
||||
contributors: 'Test Author',
|
||||
ean: '1234567890123',
|
||||
format: 'Hardcover',
|
||||
formatDetail: '200 pages',
|
||||
productGroup: 'BOOK',
|
||||
},
|
||||
} as ReceiptItem;
|
||||
|
||||
const mockProductGroups = [
|
||||
{ key: 'BOOK', value: 'Books' },
|
||||
{ key: 'MAGAZINE', value: 'Magazines' },
|
||||
{ key: 'DVD', value: 'DVDs' },
|
||||
];
|
||||
|
||||
const mockRemissionProductGroupService = {
|
||||
fetchProductGroups: vi.fn().mockResolvedValue(mockProductGroups),
|
||||
};
|
||||
|
||||
const mockRemissionReturnReceiptService = {
|
||||
removeReturnItemFromReturnReceipt: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RemissionReturnReceiptDetailsItemComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: RemissionProductGroupService,
|
||||
useValue: mockRemissionProductGroupService,
|
||||
},
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(RemissionReturnReceiptDetailsItemComponent, {
|
||||
remove: {
|
||||
imports: [
|
||||
ProductImageDirective,
|
||||
ProductRouterLinkDirective,
|
||||
ProductFormatComponent,
|
||||
IconButtonComponent,
|
||||
],
|
||||
},
|
||||
add: {
|
||||
imports: [
|
||||
MockDirective(ProductImageDirective),
|
||||
MockDirective(ProductRouterLinkDirective),
|
||||
MockComponent(ProductFormatComponent),
|
||||
MockComponent(IconButtonComponent),
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(
|
||||
RemissionReturnReceiptDetailsItemComponent,
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt.mockClear();
|
||||
});
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have required item input', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
expect(component.item()).toEqual(mockReceiptItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component with valid receipt item', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
});
|
||||
|
||||
it('should display receipt item data', () => {
|
||||
expect(component.item()).toEqual(mockReceiptItem);
|
||||
expect(component.item().id).toBe(1);
|
||||
expect(component.item().quantity).toBe(5);
|
||||
expect(component.item().product.name).toBe('Test Product');
|
||||
});
|
||||
|
||||
it('should handle product information correctly', () => {
|
||||
const item = component.item();
|
||||
|
||||
expect(item.product.name).toBe('Test Product');
|
||||
expect(item.product.contributors).toBe('Test Author');
|
||||
expect(item.product.ean).toBe('1234567890123');
|
||||
expect(item.product.format).toBe('Hardcover');
|
||||
expect(item.product.formatDetail).toBe('200 pages');
|
||||
expect(item.product.productGroup).toBe('BOOK');
|
||||
});
|
||||
|
||||
it('should handle quantity correctly', () => {
|
||||
expect(component.item().quantity).toBe(5);
|
||||
});
|
||||
|
||||
it('should have default removeable value', () => {
|
||||
expect(component.removeable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component with different receipt item data', () => {
|
||||
it('should handle different quantity values', () => {
|
||||
const differentItem = {
|
||||
...mockReceiptItem,
|
||||
quantity: 10,
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('item', differentItem);
|
||||
|
||||
expect(component.item().quantity).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle different product information', () => {
|
||||
const differentItem: ReceiptItem = {
|
||||
...mockReceiptItem,
|
||||
product: {
|
||||
...mockReceiptItem.product,
|
||||
name: 'Different Product',
|
||||
contributors: 'Different Author',
|
||||
productGroup: 'MAGAZINE',
|
||||
},
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('item', differentItem);
|
||||
|
||||
expect(component.item().product.name).toBe('Different Product');
|
||||
expect(component.item().product.contributors).toBe('Different Author');
|
||||
expect(component.item().product.productGroup).toBe('MAGAZINE');
|
||||
});
|
||||
|
||||
it('should handle item with different ID', () => {
|
||||
const differentItem = {
|
||||
...mockReceiptItem,
|
||||
id: 999,
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('item', differentItem);
|
||||
|
||||
expect(component.item().id).toBe(999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component reactivity', () => {
|
||||
it('should update when item input changes', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
expect(component.item().quantity).toBe(5);
|
||||
expect(component.item().product.name).toBe('Test Product');
|
||||
|
||||
// Change the item
|
||||
const newItem = {
|
||||
...mockReceiptItem,
|
||||
id: 2,
|
||||
quantity: 3,
|
||||
product: {
|
||||
...mockReceiptItem.product,
|
||||
name: 'Updated Product',
|
||||
},
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('item', newItem);
|
||||
|
||||
expect(component.item().id).toBe(2);
|
||||
expect(component.item().quantity).toBe(3);
|
||||
expect(component.item().product.name).toBe('Updated Product');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Removeable input', () => {
|
||||
it('should default to false when not provided', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
expect(component.removeable()).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept true value', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('removeable', true);
|
||||
|
||||
expect(component.removeable()).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept false value', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('removeable', false);
|
||||
|
||||
expect(component.removeable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Product Group functionality', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
});
|
||||
|
||||
it('should initialize productGroupResource', () => {
|
||||
expect(component.productGroupResource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should compute productGroupDetail correctly when resource has data', () => {
|
||||
// Mock the resource value directly
|
||||
vi.spyOn(component.productGroupResource, 'value').mockReturnValue(
|
||||
mockProductGroups,
|
||||
);
|
||||
|
||||
// The productGroupDetail should find the matching product group
|
||||
expect(component.productGroupDetail()).toBe('Books');
|
||||
});
|
||||
|
||||
it('should return empty string when resource value is undefined', () => {
|
||||
// Mock the resource to return undefined
|
||||
vi.spyOn(component.productGroupResource, 'value').mockReturnValue(
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(component.productGroupDetail()).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when product group not found', () => {
|
||||
const differentItem: ReceiptItem = {
|
||||
...mockReceiptItem,
|
||||
product: {
|
||||
...mockReceiptItem.product,
|
||||
productGroup: 'UNKNOWN',
|
||||
},
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('item', differentItem);
|
||||
|
||||
// Mock the resource value
|
||||
vi.spyOn(component.productGroupResource, 'value').mockReturnValue(
|
||||
mockProductGroups,
|
||||
);
|
||||
|
||||
expect(component.productGroupDetail()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Icon button rendering', () => {
|
||||
it('should render icon button when removeable is true', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('removeable', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const iconButton = fixture.nativeElement.querySelector('ui-icon-button');
|
||||
expect(iconButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render icon button when removeable is false', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('removeable', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const iconButton = fixture.nativeElement.querySelector('ui-icon-button');
|
||||
expect(iconButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render icon button with correct properties', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('removeable', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const iconButton = fixture.debugElement.query(
|
||||
By.css('ui-icon-button'),
|
||||
)?.componentInstance;
|
||||
|
||||
expect(iconButton).toBeTruthy();
|
||||
expect(iconButton.name).toBe('isaActionClose');
|
||||
expect(iconButton.size).toBe('large');
|
||||
expect(iconButton.color).toBe('secondary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template rendering', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render product image with correct attributes', () => {
|
||||
const img = fixture.nativeElement.querySelector('img');
|
||||
|
||||
expect(img).toBeTruthy();
|
||||
expect(img.getAttribute('alt')).toBe('Test Product');
|
||||
expect(img.classList.contains('w-full')).toBe(true);
|
||||
expect(img.classList.contains('max-h-[5.125rem]')).toBe(true);
|
||||
expect(img.classList.contains('object-contain')).toBe(true);
|
||||
});
|
||||
|
||||
it('should render product contributors', () => {
|
||||
const contributorsElement = fixture.nativeElement.querySelector(
|
||||
'.isa-text-body-2-bold',
|
||||
);
|
||||
|
||||
expect(contributorsElement).toBeTruthy();
|
||||
expect(contributorsElement.textContent).toBe('Test Author');
|
||||
});
|
||||
|
||||
it('should render product name', () => {
|
||||
const nameElement = fixture.nativeElement.querySelector(
|
||||
'.isa-text-body-2-regular',
|
||||
);
|
||||
|
||||
expect(nameElement).toBeTruthy();
|
||||
expect(nameElement.textContent).toBe('Test Product');
|
||||
});
|
||||
|
||||
it('should render bullet list items', () => {
|
||||
const bulletListItems = fixture.nativeElement.querySelectorAll(
|
||||
'ui-bullet-list-item',
|
||||
);
|
||||
|
||||
expect(bulletListItems.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component imports', () => {
|
||||
it('should have ProductImageDirective import', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
// Component should be created successfully with mocked imports
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have ProductRouterLinkDirective import', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
// Component should be created successfully with mocked imports
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have ProductFormatComponent import', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
|
||||
// Component should be created successfully with mocked imports
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E Testing Attributes', () => {
|
||||
it('should consider adding data-what and data-which attributes for E2E testing', () => {
|
||||
// This test serves as a reminder that E2E testing attributes
|
||||
// should be added to the template for better testability.
|
||||
// Currently the template does not have these attributes.
|
||||
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostElement = fixture.nativeElement;
|
||||
|
||||
// Verify the component renders (basic check)
|
||||
expect(hostElement).toBeTruthy();
|
||||
|
||||
// Note: In a future update, the template should include:
|
||||
// - data-what="receipt-item" on the host or main container
|
||||
// - data-which="receipt-item-details"
|
||||
// - [attr.data-item-id]="item().id" for dynamic identification
|
||||
// This would improve E2E test reliability and maintainability
|
||||
});
|
||||
});
|
||||
|
||||
describe('New inputs - receiptId and returnId', () => {
|
||||
it('should accept receiptId input', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('receiptId', 123);
|
||||
|
||||
expect(component.receiptId()).toBe(123);
|
||||
});
|
||||
|
||||
it('should accept returnId input', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('returnId', 456);
|
||||
|
||||
expect(component.returnId()).toBe(456);
|
||||
});
|
||||
|
||||
it('should handle both receiptId and returnId inputs together', () => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('receiptId', 123);
|
||||
fixture.componentRef.setInput('returnId', 456);
|
||||
|
||||
expect(component.receiptId()).toBe(123);
|
||||
expect(component.returnId()).toBe(456);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove functionality', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockReceiptItem);
|
||||
fixture.componentRef.setInput('receiptId', 123);
|
||||
fixture.componentRef.setInput('returnId', 456);
|
||||
});
|
||||
|
||||
it('should initialize removing signal as false', () => {
|
||||
expect(component.removing()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call service and emit removed event on successful remove', async () => {
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
|
||||
let emittedItem: ReceiptItem | undefined;
|
||||
component.removed.subscribe((item) => {
|
||||
emittedItem = item;
|
||||
});
|
||||
|
||||
await component.remove();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt,
|
||||
).toHaveBeenCalledWith({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
receiptItemId: 1,
|
||||
});
|
||||
expect(emittedItem).toEqual(mockReceiptItem);
|
||||
expect(component.removing()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle remove error gracefully', async () => {
|
||||
const mockError = new Error('Remove failed');
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt.mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
let emittedItem: ReceiptItem | undefined;
|
||||
component.removed.subscribe((item) => {
|
||||
emittedItem = item;
|
||||
});
|
||||
|
||||
await component.remove();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt,
|
||||
).toHaveBeenCalledWith({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
receiptItemId: 1,
|
||||
});
|
||||
expect(emittedItem).toBeUndefined();
|
||||
expect(component.removing()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call service if already removing', async () => {
|
||||
component.removing.set(true);
|
||||
|
||||
await component.remove();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.removeReturnItemFromReturnReceipt,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import {
|
||||
ReceiptItem,
|
||||
RemissionReturnReceiptService,
|
||||
ReturnItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
</div>
|
||||
<div></div>
|
||||
<remi-remission-return-receipt-details-card
|
||||
[receipt]="returnResource.value()"
|
||||
[loading]="returnResource.isLoading()"
|
||||
[return]="returnData()"
|
||||
[loading]="returnLoading()"
|
||||
></remi-remission-return-receipt-details-card>
|
||||
|
||||
@let items = returnResource.value()?.items;
|
||||
@let items = receiptItems();
|
||||
|
||||
@if (returnResource.isLoading()) {
|
||||
@if (returnLoading()) {
|
||||
<div class="text-center">
|
||||
<ui-icon-button
|
||||
class="animate-spin"
|
||||
@@ -33,31 +33,26 @@
|
||||
color="neutral"
|
||||
></ui-icon-button>
|
||||
</div>
|
||||
} @else if (items.length === 0) {
|
||||
} @else if (items?.length === 0 && !returnData()?.completed) {
|
||||
<div class="flex items-center justify-center">
|
||||
<ui-empty-state
|
||||
[title]="emptyWbsTitle"
|
||||
[description]="emptyWbsDescription"
|
||||
appearance="noArticles"
|
||||
>
|
||||
<button
|
||||
<lib-remission-return-receipt-actions
|
||||
class="mt-[1.5rem]"
|
||||
uiButton
|
||||
type="button"
|
||||
appearance="secondary"
|
||||
size="large"
|
||||
[disabled]="store.remissionStarted()"
|
||||
(click)="continueRemission()"
|
||||
>
|
||||
Jetzt befüllen
|
||||
</button>
|
||||
[remissionReturn]="returnData()"
|
||||
[displayDeleteAction]="false"
|
||||
(reloadData)="returnResource.reload()"
|
||||
></lib-remission-return-receipt-actions>
|
||||
</ui-empty-state>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="bg-isa-white rounded-2xl p-6 grid grid-flow-row gap-6">
|
||||
@for (item of items; track item.id; let last = $last) {
|
||||
<remi-remission-return-receipt-details-item
|
||||
[item]="item.data"
|
||||
[item]="item"
|
||||
[removeable]="canRemoveItems()"
|
||||
[receiptId]="receiptId()"
|
||||
[returnId]="returnId()"
|
||||
@@ -69,22 +64,11 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!returnResource.isLoading() && !returnResource.value()?.completed) {
|
||||
<ui-stateful-button
|
||||
class="fixed right-6 bottom-6"
|
||||
(clicked)="completeReturn()"
|
||||
[(state)]="completeReturnState"
|
||||
defaultContent="Wanne abschließen"
|
||||
defaultWidth="13rem"
|
||||
[errorContent]="completeReturnError()"
|
||||
errorWidth="32rem"
|
||||
errorAction="Erneut versuchen"
|
||||
(action)="completeReturn()"
|
||||
successContent="Wanne abgeschlossen"
|
||||
successWidth="20rem"
|
||||
[pending]="completingReturn()"
|
||||
size="large"
|
||||
color="brand"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
@if (!returnLoading() && !returnData()?.completed) {
|
||||
<lib-remission-return-receipt-complete
|
||||
[returnId]="returnId()"
|
||||
[receiptId]="receiptId()"
|
||||
[itemsLength]="items?.length"
|
||||
(reloadData)="returnResource.reload()"
|
||||
></lib-remission-return-receipt-complete>
|
||||
}
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MockComponent, MockProvider } from 'ng-mocks';
|
||||
import { signal } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { RemissionReturnReceiptDetailsComponent } from './remission-return-receipt-details.component';
|
||||
import { RemissionReturnReceiptDetailsCardComponent } from './remission-return-receipt-details-card.component';
|
||||
import { RemissionReturnReceiptDetailsItemComponent } from './remission-return-receipt-details-item.component';
|
||||
import {
|
||||
Receipt,
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
} from '@isa/remission/data-access';
|
||||
|
||||
// Mock the resource function
|
||||
vi.mock('./resources/remission-return-receipt.resource', () => ({
|
||||
createRemissionReturnReceiptResource: vi.fn(() => ({
|
||||
value: signal(null),
|
||||
isLoading: signal(false),
|
||||
error: signal(null),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('RemissionReturnReceiptDetailsComponent', () => {
|
||||
let component: RemissionReturnReceiptDetailsComponent;
|
||||
let fixture: ComponentFixture<RemissionReturnReceiptDetailsComponent>;
|
||||
|
||||
const mockReceipt: Receipt = {
|
||||
id: 123,
|
||||
receiptNumber: 'RR-2024-001234-ABC',
|
||||
items: [],
|
||||
completed: true,
|
||||
created: new Date('2024-01-15T10:30:00Z'),
|
||||
} as Receipt;
|
||||
|
||||
const mockRemissionReturnReceiptService = {
|
||||
completeReturnReceiptAndReturn: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RemissionReturnReceiptDetailsComponent],
|
||||
providers: [
|
||||
MockProvider(Location, { back: vi.fn() }),
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
MockProvider(RemissionStore, {
|
||||
returnId: signal(123),
|
||||
receiptId: signal(456),
|
||||
finishRemission: vi.fn(),
|
||||
}),
|
||||
],
|
||||
})
|
||||
.overrideComponent(RemissionReturnReceiptDetailsComponent, {
|
||||
remove: {
|
||||
imports: [
|
||||
RemissionReturnReceiptDetailsCardComponent,
|
||||
RemissionReturnReceiptDetailsItemComponent,
|
||||
],
|
||||
},
|
||||
add: {
|
||||
imports: [
|
||||
MockComponent(RemissionReturnReceiptDetailsCardComponent),
|
||||
MockComponent(RemissionReturnReceiptDetailsItemComponent),
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemissionReturnReceiptDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have required inputs', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
expect(component.returnId()).toBe(123);
|
||||
expect(component.receiptId()).toBe(456);
|
||||
});
|
||||
|
||||
it('should coerce string inputs to numbers', () => {
|
||||
fixture.componentRef.setInput('returnId', '123');
|
||||
fixture.componentRef.setInput('receiptId', '456');
|
||||
|
||||
expect(component.returnId()).toBe(123);
|
||||
expect(component.receiptId()).toBe(456);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependencies', () => {
|
||||
it('should inject Location service', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
expect(component.location).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create return resource', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
expect(component.returnResource).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiptNumber computed signal', () => {
|
||||
it('should return empty string when no receipt data', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
// Mock empty resource
|
||||
(component.returnResource as any).value = signal(null);
|
||||
|
||||
expect(component.receiptNumber()).toBe('');
|
||||
});
|
||||
|
||||
it('should extract receipt number substring correctly', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
// Mock resource with receipt data
|
||||
(component.returnResource as any).value = signal(mockReceipt);
|
||||
|
||||
// substring(6, 12) on 'RR-2024-001234-ABC' should return '4-0012'
|
||||
expect(component.receiptNumber()).toBe('4-0012');
|
||||
});
|
||||
|
||||
it('should handle undefined receipt number', () => {
|
||||
const receiptWithoutNumber = {
|
||||
...mockReceipt,
|
||||
receiptNumber: undefined,
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(receiptWithoutNumber);
|
||||
|
||||
expect(component.receiptNumber()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource reactivity', () => {
|
||||
it('should handle resource loading state', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
// Mock loading resource
|
||||
(component.returnResource as any).isLoading = signal(true);
|
||||
|
||||
expect(component.returnResource.isLoading()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle resource with data', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
// Mock resource with data
|
||||
(component.returnResource as any).value = signal(mockReceipt);
|
||||
(component.returnResource as any).isLoading = signal(false);
|
||||
|
||||
expect(component.returnResource.value()).toEqual(mockReceipt);
|
||||
expect(component.returnResource.isLoading()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canRemoveItems computed signal', () => {
|
||||
it('should return true when receipt is not completed', () => {
|
||||
const incompleteReceipt = {
|
||||
...mockReceipt,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(incompleteReceipt);
|
||||
|
||||
expect(component.canRemoveItems()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when receipt is completed', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(mockReceipt);
|
||||
|
||||
expect(component.canRemoveItems()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no receipt data', () => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
|
||||
(component.returnResource as any).value = signal(null);
|
||||
|
||||
// Fix: canRemoveItems() should be false when no data
|
||||
expect(component.canRemoveItems()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturn functionality', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
(component.returnResource as any).reload = vi.fn();
|
||||
// Reset mocks before each test to avoid call count bleed
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockClear();
|
||||
if (
|
||||
component.store.finishRemission &&
|
||||
'mockClear' in component.store.finishRemission
|
||||
) {
|
||||
(component.store.finishRemission as any).mockClear();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize completion state signals', () => {
|
||||
expect(component.completeReturnState()).toBe('default');
|
||||
expect(component.completingReturn()).toBe(false);
|
||||
expect(component.completeReturnError()).toBe(null);
|
||||
});
|
||||
|
||||
it('should complete return successfully', async () => {
|
||||
const mockCompletedReturn = { ...mockReceipt, completed: true };
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockCompletedReturn,
|
||||
);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.completeReturnState()).toBe('success');
|
||||
expect(component.completingReturn()).toBe(false);
|
||||
expect(component.completeReturnError()).toBe(null);
|
||||
expect(component.returnResource.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle completion error', async () => {
|
||||
const mockError = new Error('Completion failed');
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.completeReturnState()).toBe('error');
|
||||
expect(component.completingReturn()).toBe(false);
|
||||
expect(component.completeReturnError()).toBe('Completion failed');
|
||||
expect(component.returnResource.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle non-Error objects', async () => {
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockRejectedValue(
|
||||
'String error',
|
||||
);
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(component.completeReturnState()).toBe('error');
|
||||
expect(component.completeReturnError()).toBe(
|
||||
'Wanne konnte nicht abgeschlossen werden',
|
||||
);
|
||||
});
|
||||
|
||||
it('should call finishRemission on store', async () => {
|
||||
// Fix: ensure the mock is reset and tracked
|
||||
if (
|
||||
component.store.finishRemission &&
|
||||
'mockClear' in component.store.finishRemission
|
||||
) {
|
||||
(component.store.finishRemission as any).mockClear();
|
||||
}
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
{},
|
||||
);
|
||||
await component.completeReturn();
|
||||
expect(component.store.finishRemission).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not process if already completing', async () => {
|
||||
// Fix: ensure no calls are made if already completing
|
||||
component.completingReturn.set(true);
|
||||
|
||||
// Clear any previous calls
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockClear();
|
||||
|
||||
await component.completeReturn();
|
||||
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,43 +4,26 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
|
||||
import {
|
||||
ButtonComponent,
|
||||
IconButtonComponent,
|
||||
StatefulButtonComponent,
|
||||
StatefulButtonState,
|
||||
} from '@isa/ui/buttons';
|
||||
import { ButtonComponent, IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronLeft, isaLoading } from '@isa/icons';
|
||||
import { RemissionReturnReceiptDetailsCardComponent } from './remission-return-receipt-details-card.component';
|
||||
import { RemissionReturnReceiptDetailsItemComponent } from './remission-return-receipt-details-item.component';
|
||||
import { Location } from '@angular/common';
|
||||
import { createRemissionReturnReceiptResource } from './resources/remission-return-receipt.resource';
|
||||
import { createReturnResource } from './resources/return.resource';
|
||||
import {
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
getReceiptItemsFromReturn,
|
||||
getReceiptNumberFromReturn,
|
||||
} from '@isa/remission/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import { EMPTY_WBS_DESCRIPTION, EMPTY_WBS_TITLE } from './constants';
|
||||
import {
|
||||
RemissionReturnReceiptActionsComponent,
|
||||
RemissionReturnReceiptCompleteComponent,
|
||||
} from '@isa/remission/shared/return-receipt-actions';
|
||||
|
||||
/**
|
||||
* Component for displaying detailed information about a remission return receipt.
|
||||
* Shows receipt header information and individual receipt items.
|
||||
*
|
||||
* @component
|
||||
* @selector remi-remission-return-receipt-details
|
||||
* @standalone
|
||||
*
|
||||
* @example
|
||||
* <remi-remission-return-receipt-details
|
||||
* [returnId]="123"
|
||||
* [receiptId]="456">
|
||||
* </remi-remission-return-receipt-details>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-remission-return-receipt-details',
|
||||
templateUrl: './remission-return-receipt-details.component.html',
|
||||
@@ -53,23 +36,18 @@ import { EMPTY_WBS_DESCRIPTION, EMPTY_WBS_TITLE } from './constants';
|
||||
NgIcon,
|
||||
RemissionReturnReceiptDetailsCardComponent,
|
||||
RemissionReturnReceiptDetailsItemComponent,
|
||||
StatefulButtonComponent,
|
||||
EmptyStateComponent,
|
||||
RemissionReturnReceiptActionsComponent,
|
||||
RemissionReturnReceiptCompleteComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionChevronLeft, isaLoading })],
|
||||
})
|
||||
export class RemissionReturnReceiptDetailsComponent {
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionReturnReceiptDetailsComponent',
|
||||
}));
|
||||
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/** Title for the empty state when no remission return receipt is available */
|
||||
emptyWbsTitle = EMPTY_WBS_TITLE;
|
||||
emptyWbsDescription = EMPTY_WBS_DESCRIPTION;
|
||||
|
||||
/** Instance of the RemissionStore for managing remission state */
|
||||
store = inject(RemissionStore);
|
||||
/** Description for the empty state when no remission return receipt is available */
|
||||
emptyWbsDescription = EMPTY_WBS_DESCRIPTION;
|
||||
|
||||
/** Angular Location service for navigation */
|
||||
location = inject(Location);
|
||||
@@ -95,66 +73,36 @@ export class RemissionReturnReceiptDetailsComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Resource that fetches the return receipt data based on the provided IDs.
|
||||
* Automatically updates when input IDs change.
|
||||
* Computed signal that retrieves the current remission return receipt.
|
||||
* This is used to display detailed information about the return receipt.
|
||||
* @returns {Return} The remission return receipt data
|
||||
*/
|
||||
returnResource = createRemissionReturnReceiptResource(() => ({
|
||||
returnResource = createReturnResource(() => ({
|
||||
returnId: this.returnId(),
|
||||
receiptId: this.receiptId(),
|
||||
eagerLoading: 3,
|
||||
}));
|
||||
|
||||
returnLoading = computed(() => this.returnResource.isLoading());
|
||||
|
||||
returnData = computed(() => this.returnResource.value());
|
||||
|
||||
/**
|
||||
* Computed signal that extracts the receipt number from the resource.
|
||||
* Returns a substring of the receipt number (characters 6-12) for display.
|
||||
* @returns {string} The formatted receipt number or empty string if not available
|
||||
* Computed signal that retrieves the receipt number from the return data.
|
||||
* Uses the helper function to get the receipt number.
|
||||
* @returns {string} The receipt number from the return
|
||||
*/
|
||||
receiptNumber = computed(() => {
|
||||
const ret = this.returnResource.value();
|
||||
if (!ret) {
|
||||
return '';
|
||||
}
|
||||
const returnData = this.returnData();
|
||||
return getReceiptNumberFromReturn(returnData!);
|
||||
});
|
||||
|
||||
return ret.receiptNumber?.substring(6, 12) || '';
|
||||
receiptItems = computed(() => {
|
||||
const returnData = this.returnData();
|
||||
return getReceiptItemsFromReturn(returnData!);
|
||||
});
|
||||
|
||||
canRemoveItems = computed(() => {
|
||||
const ret = this.returnResource.value();
|
||||
return !!ret && !ret.completed;
|
||||
const returnData = this.returnData();
|
||||
return !!returnData && !returnData.completed;
|
||||
});
|
||||
|
||||
completeReturnState = signal<StatefulButtonState>('default');
|
||||
completingReturn = signal(false);
|
||||
completeReturnError = signal<string | null>(null);
|
||||
|
||||
async continueRemission() {
|
||||
this.store.startRemission({
|
||||
returnId: this.returnId(),
|
||||
receiptId: this.receiptId(),
|
||||
});
|
||||
}
|
||||
|
||||
async completeReturn() {
|
||||
if (this.completingReturn()) {
|
||||
return;
|
||||
}
|
||||
this.completingReturn.set(true);
|
||||
try {
|
||||
await this.#remissionReturnReceiptService.completeReturnReceiptAndReturn({
|
||||
returnId: this.returnId(),
|
||||
receiptId: this.receiptId(),
|
||||
});
|
||||
this.store.finishRemission();
|
||||
this.completeReturnState.set('success');
|
||||
this.returnResource.reload();
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to complete return', error);
|
||||
this.completeReturnError.set(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Wanne konnte nicht abgeschlossen werden',
|
||||
);
|
||||
this.completeReturnState.set('error');
|
||||
}
|
||||
this.completingReturn.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './product-group.resource';
|
||||
export * from './remission-return-receipt.resource';
|
||||
export * from './return.resource';
|
||||
export * from './supplier.resource';
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { runInInjectionContext, Injector } from '@angular/core';
|
||||
import { MockProvider } from 'ng-mocks';
|
||||
import { createRemissionReturnReceiptResource } from './remission-return-receipt.resource';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import { Receipt } from '@isa/remission/data-access';
|
||||
|
||||
describe('createRemissionReturnReceiptResource', () => {
|
||||
let mockService: any;
|
||||
let mockReceipt: Receipt;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReceipt = {
|
||||
id: 123,
|
||||
receiptNumber: 'RR-2024-001234-ABC',
|
||||
completed: true,
|
||||
created: new Date('2024-01-15T10:30:00Z'),
|
||||
supplier: {
|
||||
id: 456,
|
||||
name: 'Test Supplier',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
quantity: 5,
|
||||
product: { id: 1, name: 'Product 1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
packages: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
packageNumber: 'PKG-001',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as Receipt;
|
||||
|
||||
mockService = {
|
||||
fetchRemissionReturnReceipt: vi.fn().mockResolvedValue(mockReceipt),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
MockProvider(RemissionReturnReceiptService, mockService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Creation', () => {
|
||||
it('should create resource successfully', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
expect(resource.value).toBeDefined();
|
||||
expect(resource.isLoading).toBeDefined();
|
||||
expect(resource.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should inject RemissionReturnReceiptService', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
expect(mockService).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Parameters', () => {
|
||||
it('should handle numeric parameters', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle string parameters', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: '123',
|
||||
returnId: '456',
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle mixed parameter types', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: '456',
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource State Management', () => {
|
||||
it('should provide loading state', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource.isLoading).toBeDefined();
|
||||
expect(typeof resource.isLoading).toBe('function');
|
||||
});
|
||||
|
||||
it('should provide error state', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource.error).toBeDefined();
|
||||
expect(typeof resource.error).toBe('function');
|
||||
});
|
||||
|
||||
it('should provide value state', () => {
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(resource.value).toBeDefined();
|
||||
expect(typeof resource.value).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Function', () => {
|
||||
it('should create resource function correctly', () => {
|
||||
const createResourceFn = () => createRemissionReturnReceiptResource(() => ({
|
||||
receiptId: 123,
|
||||
returnId: 456,
|
||||
}));
|
||||
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), createResourceFn);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
expect(typeof resource.value).toBe('function');
|
||||
expect(typeof resource.isLoading).toBe('function');
|
||||
expect(typeof resource.error).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle resource initialization', () => {
|
||||
const params = { receiptId: 123, returnId: 456 };
|
||||
|
||||
const resource = runInInjectionContext(TestBed.inject(Injector), () =>
|
||||
createRemissionReturnReceiptResource(() => params)
|
||||
);
|
||||
|
||||
expect(resource).toBeDefined();
|
||||
expect(resource.value).toBeDefined();
|
||||
expect(resource.isLoading).toBeDefined();
|
||||
expect(resource.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { resource, inject } from '@angular/core';
|
||||
import {
|
||||
RemissionReturnReceiptService,
|
||||
FetchRemissionReturnParams,
|
||||
} from '@isa/remission/data-access';
|
||||
|
||||
/**
|
||||
* Creates an Angular resource for fetching a specific remission return receipt.
|
||||
* The resource automatically manages loading state and caching.
|
||||
*
|
||||
* @function createRemissionReturnReceiptResource
|
||||
* @param {Function} params - Function that returns the receipt and return IDs
|
||||
* @param {string | number} params.receiptId - ID of the receipt to fetch
|
||||
* @param {string | number} params.returnId - ID of the return containing the receipt
|
||||
* @returns {Resource} Angular resource that manages the receipt data
|
||||
*
|
||||
* @example
|
||||
* const receiptResource = createRemissionReturnReceiptResource(() => ({
|
||||
* receiptId: '123',
|
||||
* returnId: '456'
|
||||
* }));
|
||||
*
|
||||
* // Access the resource value
|
||||
* const receipt = receiptResource.value();
|
||||
* const isLoading = receiptResource.isLoading();
|
||||
*/
|
||||
export const createRemissionReturnReceiptResource = (
|
||||
params: () => FetchRemissionReturnParams,
|
||||
) => {
|
||||
const remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
return resource({
|
||||
params,
|
||||
loader: ({ abortSignal, params }) =>
|
||||
remissionReturnReceiptService.fetchRemissionReturnReceipt(
|
||||
params,
|
||||
abortSignal,
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { resource, inject } from '@angular/core';
|
||||
import {
|
||||
FetchReturn,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
|
||||
/**
|
||||
* Resource for creating a new remission return.
|
||||
* It uses the RemissionReturnReceiptService to handle the creation logic.
|
||||
* @param {Function} params - Function that returns parameters for creating a return
|
||||
* @return {Resource} Angular resource that manages the return creation data
|
||||
*/
|
||||
export const createReturnResource = (params: () => FetchReturn) => {
|
||||
const remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
return resource({
|
||||
params,
|
||||
loader: ({ abortSignal, params }) =>
|
||||
remissionReturnReceiptService.fetchReturn(params, abortSignal),
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
<remi-return-receipt-list-card></remi-return-receipt-list-card>
|
||||
|
||||
<div class="flex flex-rows justify-end">
|
||||
<filter-order-by-toolbar class="w-[44.375rem]"></filter-order-by-toolbar>
|
||||
<filter-controls-panel></filter-controls-panel>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-rows grid-cols-1 gap-4">
|
||||
@@ -7,6 +9,7 @@
|
||||
<a [routerLink]="[remissionReturn[0].id, remissionReturn[1].id]">
|
||||
<remi-return-receipt-list-item
|
||||
[remissionReturn]="remissionReturn[0]"
|
||||
(reloadList)="reloadList()"
|
||||
></remi-return-receipt-list-item>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row gap-8 p-6;
|
||||
@apply w-full grid grid-flow-row gap-4 mt-5 isa-desktop:mt-6 overflow-x-hidden;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { MockProvider } from 'ng-mocks';
|
||||
import { RemissionReturnReceiptListComponent } from './remission-return-receipt-list.component';
|
||||
import {
|
||||
RemissionReturnReceiptService,
|
||||
Return,
|
||||
Receipt,
|
||||
} from '@isa/remission/data-access';
|
||||
import { Return, Receipt } from '@isa/remission/data-access';
|
||||
import { FilterService } from '@isa/shared/filter';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
// Mock the filter providers
|
||||
vi.mock('@isa/shared/filter', async () => {
|
||||
@@ -22,6 +16,12 @@ vi.mock('@isa/shared/filter', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the resources
|
||||
vi.mock('./resources', () => ({
|
||||
completedRemissionReturnsResource: vi.fn(),
|
||||
incompletedRemissionReturnsResource: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
@Component({
|
||||
selector: 'remi-return-receipt-list-item',
|
||||
@@ -31,21 +31,25 @@ vi.mock('@isa/shared/filter', async () => {
|
||||
class MockReturnReceiptListItemComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'remi-order-by-toolbar',
|
||||
template: '<div>Mock Order By Toolbar</div>',
|
||||
selector: 'remi-remission-return-receipt-list-card',
|
||||
template: '<div>Mock Return Receipt List Card</div>',
|
||||
standalone: true,
|
||||
})
|
||||
class MockOrderByToolbarComponent {}
|
||||
class MockRemissionReturnReceiptListCardComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'isa-filter-controls-panel',
|
||||
template: '<div>Mock Filter Controls Panel</div>',
|
||||
standalone: true,
|
||||
})
|
||||
class MockFilterControlsPanelComponent {}
|
||||
|
||||
describe('RemissionReturnReceiptListComponent', () => {
|
||||
let component: RemissionReturnReceiptListComponent;
|
||||
let fixture: ComponentFixture<RemissionReturnReceiptListComponent>;
|
||||
let mockRemissionReturnReceiptService: {
|
||||
fetchRemissionReturnReceipts: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockFilterService: {
|
||||
orderBy: ReturnType<typeof signal>;
|
||||
};
|
||||
let mockFilterService: any;
|
||||
let mockCompletedResource: any;
|
||||
let mockIncompletedResource: any;
|
||||
|
||||
const mockCompletedReturn: Return = {
|
||||
id: 1,
|
||||
@@ -81,54 +85,58 @@ describe('RemissionReturnReceiptListComponent', () => {
|
||||
],
|
||||
} as Return;
|
||||
|
||||
const mockReturnWithoutReceiptData: Return = {
|
||||
id: 3,
|
||||
completed: '2024-01-17T10:30:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 103,
|
||||
data: undefined,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
const mockReturns = [mockCompletedReturn, mockIncompletedReturn];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Arrange: Setup mocks
|
||||
mockRemissionReturnReceiptService = {
|
||||
fetchRemissionReturnReceipts: vi.fn().mockReturnValue(of(mockReturns)),
|
||||
};
|
||||
|
||||
// Setup mocks
|
||||
mockFilterService = {
|
||||
orderBy: signal([]),
|
||||
inputs: signal([]),
|
||||
groups: signal([]),
|
||||
queryParams: signal({}),
|
||||
query: signal({ filter: {}, input: {}, orderBy: [] }),
|
||||
isEmpty: signal(true),
|
||||
isDefaultFilter: signal(true),
|
||||
selectedFilterCount: signal(0),
|
||||
};
|
||||
|
||||
mockCompletedResource = {
|
||||
value: vi.fn().mockReturnValue([mockCompletedReturn]),
|
||||
reload: vi.fn(),
|
||||
isLoading: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
|
||||
mockIncompletedResource = {
|
||||
value: vi.fn().mockReturnValue([mockIncompletedReturn]),
|
||||
reload: vi.fn(),
|
||||
isLoading: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
|
||||
// Mock the resource functions
|
||||
const {
|
||||
completedRemissionReturnsResource,
|
||||
incompletedRemissionReturnsResource,
|
||||
} = await import('./resources');
|
||||
vi.mocked(completedRemissionReturnsResource).mockReturnValue(
|
||||
mockCompletedResource,
|
||||
);
|
||||
vi.mocked(incompletedRemissionReturnsResource).mockReturnValue(
|
||||
mockIncompletedResource,
|
||||
);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RemissionReturnReceiptListComponent,
|
||||
MockReturnReceiptListItemComponent,
|
||||
MockOrderByToolbarComponent,
|
||||
],
|
||||
providers: [
|
||||
MockProvider(
|
||||
RemissionReturnReceiptService,
|
||||
mockRemissionReturnReceiptService
|
||||
),
|
||||
{
|
||||
provide: FilterService,
|
||||
useValue: mockFilterService,
|
||||
},
|
||||
],
|
||||
imports: [RemissionReturnReceiptListComponent],
|
||||
providers: [{ provide: FilterService, useValue: mockFilterService }],
|
||||
})
|
||||
.overrideComponent(RemissionReturnReceiptListComponent, {
|
||||
remove: {
|
||||
imports: [],
|
||||
imports: [
|
||||
// Remove original components
|
||||
],
|
||||
},
|
||||
add: {
|
||||
imports: [
|
||||
MockReturnReceiptListItemComponent,
|
||||
MockOrderByToolbarComponent,
|
||||
MockRemissionReturnReceiptListCardComponent,
|
||||
MockFilterControlsPanelComponent,
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -138,102 +146,40 @@ describe('RemissionReturnReceiptListComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('Component Initialization', () => {
|
||||
it('should create the component', () => {
|
||||
// Assert
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly', () => {
|
||||
// Assert - Private fields cannot be directly tested
|
||||
// Instead, we verify the component initializes correctly with dependencies
|
||||
expect(component).toBeDefined();
|
||||
it('should initialize resources', () => {
|
||||
expect(component.completedRemissionReturnsResource).toBeDefined();
|
||||
expect(component.incompletedRemissionReturnsResource).toBeDefined();
|
||||
expect(component.orderDateBy).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render the component', () => {
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(fixture.nativeElement).toBeTruthy();
|
||||
expect(fixture.componentInstance).toBe(component);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Loading', () => {
|
||||
it('should initialize completed and incomplete resources', () => {
|
||||
// Assert
|
||||
expect(component.completedRemissionReturnsResource).toBeDefined();
|
||||
expect(component.incompletedRemissionReturnsResource).toBeDefined();
|
||||
describe('orderDateBy computed signal', () => {
|
||||
it('should return undefined when no order is selected', () => {
|
||||
mockFilterService.orderBy.set([]);
|
||||
|
||||
const orderBy = component.orderDateBy();
|
||||
|
||||
expect(orderBy).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fetch remission return receipts on component initialization', () => {
|
||||
// Arrange
|
||||
mockRemissionReturnReceiptService.fetchRemissionReturnReceipts.mockClear();
|
||||
it('should return selected order option', () => {
|
||||
const selectedOrder = { selected: true, by: 'created', dir: 'desc' };
|
||||
mockFilterService.orderBy.set([selectedOrder]);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
const orderBy = component.orderDateBy();
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.fetchRemissionReturnReceipts
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle loading state', () => {
|
||||
// Arrange
|
||||
mockRemissionReturnReceiptService.fetchRemissionReturnReceipts.mockReturnValue(
|
||||
new Promise(() => undefined) // Never resolving promise to simulate loading
|
||||
);
|
||||
|
||||
// Act
|
||||
const newFixture = TestBed.createComponent(
|
||||
RemissionReturnReceiptListComponent
|
||||
);
|
||||
const newComponent = newFixture.componentInstance;
|
||||
|
||||
// Assert
|
||||
expect(newComponent.completedRemissionReturnsResource.isLoading()).toBeDefined();
|
||||
expect(newComponent.incompletedRemissionReturnsResource.isLoading()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error state when service fails', async () => {
|
||||
// Arrange
|
||||
const errorMessage = 'Service failed';
|
||||
mockRemissionReturnReceiptService.fetchRemissionReturnReceipts.mockReturnValue(
|
||||
Promise.reject(new Error(errorMessage))
|
||||
);
|
||||
|
||||
// Act
|
||||
const errorFixture = TestBed.createComponent(
|
||||
RemissionReturnReceiptListComponent
|
||||
);
|
||||
errorFixture.detectChanges();
|
||||
await errorFixture.whenStable();
|
||||
|
||||
// Assert
|
||||
const errorComponent = errorFixture.componentInstance;
|
||||
expect(errorComponent.completedRemissionReturnsResource.error).toBeDefined();
|
||||
expect(orderBy).toBe(selectedOrder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns computed signal', () => {
|
||||
it('should combine returns with incompleted first', () => {
|
||||
// Arrange
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
mockCompletedReturn
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
mockIncompletedReturn
|
||||
]);
|
||||
|
||||
// Act
|
||||
it('should combine completed and incompleted returns', () => {
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(2);
|
||||
expect(returns[0][0]).toBe(mockIncompletedReturn);
|
||||
expect(returns[0][1]).toBe(mockIncompletedReturn.receipts[0].data);
|
||||
@@ -241,401 +187,22 @@ describe('RemissionReturnReceiptListComponent', () => {
|
||||
expect(returns[1][1]).toBe(mockCompletedReturn.receipts[0].data);
|
||||
});
|
||||
|
||||
it('should filter out receipts without data', () => {
|
||||
// Arrange
|
||||
const returnsWithNullData = [mockCompletedReturn, mockReturnWithoutReceiptData];
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue(
|
||||
returnsWithNullData
|
||||
);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
it('should handle empty returns', () => {
|
||||
mockCompletedResource.value.mockReturnValue([]);
|
||||
mockIncompletedResource.value.mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(1);
|
||||
expect(returns[0][0]).toBe(mockCompletedReturn);
|
||||
expect(returns[0][1]).toBe(mockCompletedReturn.receipts[0].data);
|
||||
});
|
||||
|
||||
it('should handle empty returns array', () => {
|
||||
// Arrange
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(0);
|
||||
expect(returns).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle null value from resource', () => {
|
||||
// Arrange
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue(
|
||||
undefined as Return[] | undefined
|
||||
);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(0);
|
||||
expect(returns).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle undefined value from resource', () => {
|
||||
// Arrange
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue(
|
||||
undefined as Return[] | undefined
|
||||
);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(0);
|
||||
expect(returns).toEqual([]);
|
||||
});
|
||||
|
||||
it('should flatten multiple receipts per return', () => {
|
||||
// Arrange
|
||||
const returnWithMultipleReceipts: Return = {
|
||||
id: 4,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 201,
|
||||
data: {
|
||||
id: 201,
|
||||
receiptNumber: 'REC-2024-201',
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
data: {
|
||||
id: 202,
|
||||
receiptNumber: 'REC-2024-202',
|
||||
created: '2024-01-15T10:00:00.000Z',
|
||||
completed: '2024-01-15T11:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
returnWithMultipleReceipts,
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(2);
|
||||
expect(returns[0][0]).toBe(returnWithMultipleReceipts);
|
||||
expect(returns[0][1]).toBe(returnWithMultipleReceipts.receipts[0].data);
|
||||
expect(returns[1][0]).toBe(returnWithMultipleReceipts);
|
||||
expect(returns[1][1]).toBe(returnWithMultipleReceipts.receipts[1].data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('orderDateBy computed signal', () => {
|
||||
it('should return undefined when no order is selected', () => {
|
||||
// Arrange
|
||||
mockFilterService.orderBy = signal([]);
|
||||
describe('reloadList method', () => {
|
||||
it('should reload both resources', () => {
|
||||
component.reloadList();
|
||||
|
||||
// Act
|
||||
const orderBy = component.orderDateBy();
|
||||
|
||||
// Assert
|
||||
expect(orderBy).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return selected order option', () => {
|
||||
// Arrange
|
||||
const selectedOrder = { selected: true, by: 'created', dir: 'desc' };
|
||||
const notSelectedOrder = { selected: false, by: 'completed', dir: 'asc' };
|
||||
|
||||
// Update the existing mockFilterService signal
|
||||
mockFilterService.orderBy.set([notSelectedOrder, selectedOrder]);
|
||||
|
||||
const newFixture = TestBed.createComponent(
|
||||
RemissionReturnReceiptListComponent
|
||||
);
|
||||
const newComponent = newFixture.componentInstance;
|
||||
|
||||
// Act
|
||||
const orderBy = newComponent.orderDateBy();
|
||||
|
||||
// Assert
|
||||
expect(orderBy).toBe(selectedOrder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sorting functionality', () => {
|
||||
it('should sort returns by created date in descending order', () => {
|
||||
// Arrange
|
||||
const orderOption = { selected: true, by: 'created', dir: 'desc' };
|
||||
mockFilterService.orderBy.set([orderOption]);
|
||||
|
||||
const olderReturn: Return = {
|
||||
id: 10,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
created: '2024-01-10T09:00:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 301,
|
||||
data: {
|
||||
id: 301,
|
||||
receiptNumber: 'REC-2024-301',
|
||||
created: '2024-01-10T09:00:00.000Z',
|
||||
completed: '2024-01-10T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
const newerReturn: Return = {
|
||||
id: 11,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
created: '2024-01-20T09:00:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 302,
|
||||
data: {
|
||||
id: 302,
|
||||
receiptNumber: 'REC-2024-302',
|
||||
created: '2024-01-20T09:00:00.000Z',
|
||||
completed: '2024-01-20T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
olderReturn,
|
||||
newerReturn,
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(2);
|
||||
expect(returns[0][0]).toBe(newerReturn); // Newer date should come first in desc order
|
||||
expect(returns[1][0]).toBe(olderReturn);
|
||||
});
|
||||
|
||||
it('should sort returns by created date in ascending order', () => {
|
||||
// Arrange
|
||||
const orderOption = { selected: true, by: 'created', dir: 'asc' };
|
||||
mockFilterService.orderBy.set([orderOption]);
|
||||
|
||||
const sortedFixture = TestBed.createComponent(
|
||||
RemissionReturnReceiptListComponent
|
||||
);
|
||||
const sortedComponent = sortedFixture.componentInstance;
|
||||
|
||||
const olderReturn: Return = {
|
||||
id: 10,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
created: '2024-01-10T09:00:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 301,
|
||||
data: {
|
||||
id: 301,
|
||||
receiptNumber: 'REC-2024-301',
|
||||
created: '2024-01-10T09:00:00.000Z',
|
||||
completed: '2024-01-10T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
const newerReturn: Return = {
|
||||
id: 11,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
created: '2024-01-20T09:00:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 302,
|
||||
data: {
|
||||
id: 302,
|
||||
receiptNumber: 'REC-2024-302',
|
||||
created: '2024-01-20T09:00:00.000Z',
|
||||
completed: '2024-01-20T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
vi.spyOn(sortedComponent.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
newerReturn,
|
||||
olderReturn,
|
||||
]);
|
||||
vi.spyOn(sortedComponent.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = sortedComponent.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(2);
|
||||
expect(returns[0][0]).toBe(olderReturn); // Older date should come first in asc order
|
||||
expect(returns[1][0]).toBe(newerReturn);
|
||||
});
|
||||
|
||||
it('should handle sorting with undefined dates', () => {
|
||||
// Arrange
|
||||
const orderOption = { selected: true, by: 'created', dir: 'desc' };
|
||||
mockFilterService.orderBy = signal([orderOption]);
|
||||
|
||||
const returnWithDate: Return = {
|
||||
id: 10,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
created: '2024-01-10T09:00:00.000Z',
|
||||
receipts: [
|
||||
{
|
||||
id: 301,
|
||||
data: {
|
||||
id: 301,
|
||||
receiptNumber: 'REC-2024-301',
|
||||
created: '2024-01-10T09:00:00.000Z',
|
||||
completed: '2024-01-10T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
const returnWithoutDate: Return = {
|
||||
id: 11,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
created: undefined,
|
||||
receipts: [
|
||||
{
|
||||
id: 302,
|
||||
data: {
|
||||
id: 302,
|
||||
receiptNumber: 'REC-2024-302',
|
||||
created: '2024-01-20T09:00:00.000Z',
|
||||
completed: '2024-01-20T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
},
|
||||
],
|
||||
} as Return;
|
||||
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
returnWithDate,
|
||||
returnWithoutDate,
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(2);
|
||||
expect(returns[0][0]).toBe(returnWithDate); // Item with date should come first
|
||||
expect(returns[1][0]).toBe(returnWithoutDate); // Undefined date goes to end
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Destruction', () => {
|
||||
it('should handle component destruction gracefully', () => {
|
||||
// Act
|
||||
fixture.destroy();
|
||||
|
||||
// Assert
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle returns with empty receipts array', () => {
|
||||
// Arrange
|
||||
const returnWithEmptyReceipts: Return = {
|
||||
id: 100,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
receipts: [],
|
||||
} as Return;
|
||||
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
returnWithEmptyReceipts,
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle mixed returns with and without receipt data', () => {
|
||||
// Arrange
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
mockCompletedReturn,
|
||||
mockReturnWithoutReceiptData
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
mockIncompletedReturn
|
||||
]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(2); // Only returns with receipt data
|
||||
expect(returns[0][0]).toBe(mockIncompletedReturn); // Incompleted first
|
||||
expect(returns[1][0]).toBe(mockCompletedReturn);
|
||||
});
|
||||
|
||||
it('should handle very large number of receipts per return', () => {
|
||||
// Arrange
|
||||
const returnWithManyReceipts: Return = {
|
||||
id: 200,
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
receipts: Array.from({ length: 100 }, (_, i) => ({
|
||||
id: 1000 + i,
|
||||
data: {
|
||||
id: 1000 + i,
|
||||
receiptNumber: `REC-2024-${1000 + i}`,
|
||||
created: '2024-01-15T09:00:00.000Z',
|
||||
completed: '2024-01-15T10:00:00.000Z',
|
||||
items: [],
|
||||
} as Receipt,
|
||||
})),
|
||||
} as Return;
|
||||
|
||||
vi.spyOn(component.completedRemissionReturnsResource, 'value').mockReturnValue([
|
||||
returnWithManyReceipts,
|
||||
]);
|
||||
vi.spyOn(component.incompletedRemissionReturnsResource, 'value').mockReturnValue([]);
|
||||
|
||||
// Act
|
||||
const returns = component.returns();
|
||||
|
||||
// Assert
|
||||
expect(returns).toHaveLength(100);
|
||||
returns.forEach(([returnData, receipt]) => {
|
||||
expect(returnData).toBe(returnWithManyReceipts);
|
||||
expect(receipt).toBeDefined();
|
||||
});
|
||||
expect(mockCompletedResource.reload).toHaveBeenCalled();
|
||||
expect(mockIncompletedResource.reload).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,29 +3,28 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
resource,
|
||||
} from '@angular/core';
|
||||
import { ReturnReceiptListItemComponent } from './return-receipt-list-item/return-receipt-list-item.component';
|
||||
import {
|
||||
Receipt,
|
||||
RemissionReturnReceiptService,
|
||||
Return,
|
||||
} from '@isa/remission/data-access';
|
||||
import { Receipt, Return } from '@isa/remission/data-access';
|
||||
import {
|
||||
provideFilter,
|
||||
withQueryParamsSync,
|
||||
withQuerySettings,
|
||||
OrderByToolbarComponent,
|
||||
FilterService,
|
||||
FilterControlsPanelComponent,
|
||||
} from '@isa/shared/filter';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { compareAsc, compareDesc, subDays } from 'date-fns';
|
||||
import { RETURN_RECEIPT_QUERY_SETTINGS } from './remission-return-receipt-list.query-settings';
|
||||
import { RemissionReturnReceiptListCardComponent } from './return-receipt-list-card/return-receipt-list-card.component';
|
||||
import {
|
||||
completedRemissionReturnsResource,
|
||||
incompletedRemissionReturnsResource,
|
||||
} from './resources';
|
||||
|
||||
/**
|
||||
* Component that displays a list of remission return receipts.
|
||||
* Fetches both completed and incomplete receipts and combines them for display.
|
||||
* Supports filtering and sorting through query parameters.
|
||||
* Component for displaying a list of remission return receipts.
|
||||
* It shows both completed and incomplete receipts, with options to filter and sort.
|
||||
*
|
||||
* @component
|
||||
* @selector remi-remission-return-receipt-list
|
||||
@@ -41,8 +40,9 @@ import { RETURN_RECEIPT_QUERY_SETTINGS } from './remission-return-receipt-list.q
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
RemissionReturnReceiptListCardComponent,
|
||||
FilterControlsPanelComponent,
|
||||
ReturnReceiptListItemComponent,
|
||||
OrderByToolbarComponent,
|
||||
RouterLink,
|
||||
],
|
||||
providers: [
|
||||
@@ -53,46 +53,41 @@ import { RETURN_RECEIPT_QUERY_SETTINGS } from './remission-return-receipt-list.q
|
||||
],
|
||||
})
|
||||
export class RemissionReturnReceiptListComponent {
|
||||
/** Private instance of the remission return receipt service */
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/** Filter service for managing filter state and operations */
|
||||
#filter = inject(FilterService);
|
||||
|
||||
/**
|
||||
* Computed signal that retrieves the currently selected order date for sorting.
|
||||
* This is used to determine how the return receipts should be ordered.
|
||||
* @returns {string | undefined} The selected order date
|
||||
*/
|
||||
orderDateBy = computed(() => this.#filter.orderBy().find((o) => o.selected));
|
||||
|
||||
/**
|
||||
* Resource that fetches completed remission return receipts.
|
||||
* Automatically loads when the component is initialized.
|
||||
*/
|
||||
completedRemissionReturnsResource = resource({
|
||||
loader: ({ abortSignal }) =>
|
||||
this.#remissionReturnReceiptService.fetchRemissionReturnReceipts(
|
||||
{ returncompleted: true, start: subDays(new Date(), 7) },
|
||||
abortSignal,
|
||||
),
|
||||
/** Resource for fetching completed remission return receipts */
|
||||
completedRemissionReturnsResource = completedRemissionReturnsResource();
|
||||
|
||||
/** Resource for fetching incomplete remission return receipts */
|
||||
incompletedRemissionReturnsResource = incompletedRemissionReturnsResource();
|
||||
|
||||
/** Computed signal that retrieves the value of the completed remission returns resource */
|
||||
completedRemissionReturnsResourceValue = computed(() => {
|
||||
return this.completedRemissionReturnsResource.value() || [];
|
||||
});
|
||||
|
||||
/** Computed signal that retrieves the value of the incomplete remission returns resource */
|
||||
incompletedRemissionReturnsResourceValue = computed(() => {
|
||||
return this.incompletedRemissionReturnsResource.value() || [];
|
||||
});
|
||||
|
||||
/**
|
||||
* Resource that fetches incomplete remission return receipts.
|
||||
* Automatically loads when the component is initialized.
|
||||
*/
|
||||
incompletedRemissionReturnsResource = resource({
|
||||
loader: ({ abortSignal }) =>
|
||||
this.#remissionReturnReceiptService.fetchRemissionReturnReceipts(
|
||||
{ returncompleted: false },
|
||||
abortSignal,
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that combines completed and incomplete returns.
|
||||
* Maps each return with its receipts into tuples for display.
|
||||
* When date ordering is selected, sorts by completion date with incomplete items first.
|
||||
* @returns {Array<[Return, Receipt]>} Array of tuples containing return and receipt pairs
|
||||
* Computed signal that combines completed and incomplete remission returns,
|
||||
* filtering out any receipts that do not have associated data.
|
||||
* It also orders the returns based on the selected order date.
|
||||
* @returns {Array<[Return, Receipt]>} Array of tuples containing Return and Receipt objects
|
||||
*/
|
||||
returns = computed(() => {
|
||||
let completed = this.completedRemissionReturnsResource.value() || [];
|
||||
let incompleted = this.incompletedRemissionReturnsResource.value() || [];
|
||||
let completed = this.completedRemissionReturnsResourceValue();
|
||||
let incompleted = this.incompletedRemissionReturnsResourceValue();
|
||||
const orderBy = this.orderDateBy();
|
||||
|
||||
if (orderBy) {
|
||||
@@ -113,8 +108,27 @@ export class RemissionReturnReceiptListComponent {
|
||||
.map((rec) => [ret, rec.data] as [Return, Receipt]),
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Reloads the completed and incomplete remission returns resources.
|
||||
* This is typically called when the user performs an action that requires
|
||||
* refreshing the list of returns, such as after adding or deleting a return.
|
||||
*/
|
||||
reloadList() {
|
||||
this.completedRemissionReturnsResource.reload();
|
||||
this.incompletedRemissionReturnsResource.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to order an array of objects by a specific key.
|
||||
* Uses a custom comparison function to sort the items.
|
||||
*
|
||||
* @param items - Array of items to be sorted
|
||||
* @param by - Key to sort by
|
||||
* @param compareFn - Comparison function for sorting
|
||||
* @returns Sorted array of items
|
||||
*/
|
||||
function orderByKey<T, K extends keyof T>(
|
||||
items: T[],
|
||||
by: K,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { inject, resource } from '@angular/core';
|
||||
import {
|
||||
FetchRemissionReturnReceipts,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Resource for fetching completed remission return receipts.
|
||||
* It retrieves receipts that are marked as completed within a specified date range.
|
||||
* @param {Function} params - Function that returns parameters for fetching receipts
|
||||
* @param {Object} params.returncompleted - Boolean indicating if the return is completed
|
||||
* @param {Date} params.start - Start date for filtering receipts
|
||||
* @return {Resource} Angular resource that manages the completed receipts data
|
||||
*/
|
||||
export const completedRemissionReturnsResource = (
|
||||
params: () => FetchRemissionReturnReceipts = () => ({
|
||||
returncompleted: true,
|
||||
start: subDays(new Date(), 7),
|
||||
}),
|
||||
) => {
|
||||
const remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
return resource({
|
||||
params,
|
||||
loader: ({ abortSignal, params }) => {
|
||||
return remissionReturnReceiptService.fetchRemissionReturnReceipts(
|
||||
params,
|
||||
abortSignal,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource for fetching incomplete remission return receipts.
|
||||
* It retrieves receipts that are not marked as completed.
|
||||
* @param {Function} params - Function that returns parameters for fetching receipts
|
||||
* @param {Object} params.returncompleted - Boolean indicating if the return is completed
|
||||
* @return {Resource} Angular resource that manages the incomplete receipts data
|
||||
*/
|
||||
export const incompletedRemissionReturnsResource = (
|
||||
params: () => FetchRemissionReturnReceipts = () => ({
|
||||
returncompleted: false,
|
||||
}),
|
||||
) => {
|
||||
const remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
return resource({
|
||||
params,
|
||||
loader: ({ abortSignal, params }) => {
|
||||
return remissionReturnReceiptService.fetchRemissionReturnReceipts(
|
||||
params,
|
||||
abortSignal,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './fetch-return-receipt-list.recource';
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="remi-return-receipt-list-card__title-container">
|
||||
<h2 class="isa-text-subtitle-1-regular">Warenbegleitscheine</h2>
|
||||
|
||||
<p class="isa-text-body-1-regular">
|
||||
Offene Warenbegleitscheine können nur gelöscht werden, wenn sie keine
|
||||
Artikel enthalten. Entfernen Sie diese, bevor Sie den Warenbegleitschein
|
||||
löschen.
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
@apply w-full flex flex-row gap-6 rounded-2xl bg-isa-neutral-400 p-6 justify-between;
|
||||
}
|
||||
|
||||
.remi-return-receipt-list-card__title-container {
|
||||
@apply flex flex-col gap-4 text-isa-neutral-900;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-return-receipt-list-card',
|
||||
templateUrl: './return-receipt-list-card.component.html',
|
||||
styleUrl: './return-receipt-list-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RemissionReturnReceiptListCardComponent {}
|
||||
@@ -1,14 +1,30 @@
|
||||
<div class="flex flex-col">
|
||||
<div>Warenbegleitschein</div>
|
||||
<div class="isa-text-body-1-bold">#{{ receiptNumber() }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-6 p-6 rounded-2xl text-isa-neutral-900 isa-text-body-1-regular"
|
||||
[class.bg-isa-white]="status() === ReceiptCompleteStatus.Offen"
|
||||
[class.bg-isa-neutral-400]="status() === ReceiptCompleteStatus.Abgeschlossen"
|
||||
>
|
||||
<div class="flex flex-row justify-start gap-6">
|
||||
<div class="flex flex-col">
|
||||
<div>Warenbegleitschein</div>
|
||||
<div class="isa-text-body-1-bold">#{{ receiptNumber() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>Anzahl Positionen</div>
|
||||
<div class="isa-text-body-1-bold">{{ itemQuantity() }}</div>
|
||||
</div>
|
||||
<div class="flex-grow"></div>
|
||||
<div class="flex flex-col">
|
||||
<div>Status</div>
|
||||
<div class="isa-text-body-1-bold">{{ status() }}</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Anzahl Positionen</div>
|
||||
<div class="isa-text-body-1-bold">{{ itemQuantity() }}</div>
|
||||
</div>
|
||||
<div class="flex-grow"></div>
|
||||
<div class="flex flex-col w-32">
|
||||
<div>Status</div>
|
||||
<div class="isa-text-body-1-bold">{{ status() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (status() === ReceiptCompleteStatus.Offen) {
|
||||
<lib-remission-return-receipt-actions
|
||||
[remissionReturn]="remissionReturn()"
|
||||
(reloadData)="reloadList.emit()"
|
||||
>
|
||||
</lib-remission-return-receipt-actions>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
:host {
|
||||
@apply flex flex-row justify-start gap-6 p-6 rounded-2xl text-isa-neutral-900 isa-text-body-1-regular;
|
||||
|
||||
&.remi-return-receipt-list-item--offen {
|
||||
@apply bg-isa-white;
|
||||
}
|
||||
|
||||
&.remi-return-receipt-list-item--abgeschlossen {
|
||||
@apply bg-isa-neutral-400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ReturnReceiptListItemComponent } from './return-receipt-list-item.component';
|
||||
import { Return } from '@isa/remission/data-access';
|
||||
import {
|
||||
Return,
|
||||
ReceiptCompleteStatus,
|
||||
getReceiptNumberFromReturn,
|
||||
getReceiptItemQuantityFromReturn,
|
||||
getReceiptStatusFromReturn,
|
||||
} from '@isa/remission/data-access';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { RemissionReturnReceiptActionsComponent } from '@isa/remission/shared/return-receipt-actions';
|
||||
|
||||
// Mock the helper functions
|
||||
vi.mock('@isa/remission/data-access', async () => {
|
||||
const actual = await vi.importActual('@isa/remission/data-access');
|
||||
return {
|
||||
...actual,
|
||||
getReceiptNumberFromReturn: vi.fn(),
|
||||
getReceiptItemQuantityFromReturn: vi.fn(),
|
||||
getReceiptStatusFromReturn: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('ReturnReceiptListItemComponent', () => {
|
||||
let component: ReturnReceiptListItemComponent;
|
||||
@@ -10,13 +30,23 @@ describe('ReturnReceiptListItemComponent', () => {
|
||||
const createMockReturn = (overrides: Partial<Return> = {}): Return =>
|
||||
({
|
||||
id: 1,
|
||||
receipts: [],
|
||||
receipts: [
|
||||
{ id: 101, items: [] },
|
||||
{ id: 102, items: [] },
|
||||
],
|
||||
...overrides,
|
||||
}) as Return;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReturnReceiptListItemComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ReturnReceiptListItemComponent,
|
||||
MockComponent(RemissionReturnReceiptActionsComponent),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReturnReceiptListItemComponent);
|
||||
@@ -25,595 +55,311 @@ describe('ReturnReceiptListItemComponent', () => {
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
fixture.componentRef.setInput('remissionReturn', createMockReturn());
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have remissionReturn as required input', () => {
|
||||
fixture.componentRef.setInput('remissionReturn', createMockReturn());
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.remissionReturn()).toBeDefined();
|
||||
expect(component.remissionReturn()).toEqual(mockReturn);
|
||||
});
|
||||
|
||||
it('should have correct host class applied', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostElement = fixture.debugElement.nativeElement;
|
||||
expect(
|
||||
hostElement.classList.contains('remi-return-receipt-list-item'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiptNumber computed signal', () => {
|
||||
it('should return "Keine Belege vorhanden" when no receipts', () => {
|
||||
const mockReturn = createMockReturn({ receipts: [] });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
describe('Computed signals', () => {
|
||||
describe('receiptNumber', () => {
|
||||
it('should return receipt number from helper function', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
const expectedReceiptNumber = '24-001';
|
||||
|
||||
expect(component.receiptNumber()).toBe('Keine Belege vorhanden');
|
||||
vi.mocked(getReceiptNumberFromReturn).mockReturnValue(
|
||||
expectedReceiptNumber,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe(expectedReceiptNumber);
|
||||
expect(getReceiptNumberFromReturn).toHaveBeenCalledWith(mockReturn);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return single receipt number substring', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
describe('itemQuantity', () => {
|
||||
it('should return item quantity from helper function', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
const expectedQuantity = 5;
|
||||
|
||||
expect(component.receiptNumber()).toBe('24-001');
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(
|
||||
expectedQuantity,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(expectedQuantity);
|
||||
expect(getReceiptItemQuantityFromReturn).toHaveBeenCalledWith(
|
||||
mockReturn,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return multiple receipt numbers joined with comma', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
data: {
|
||||
id: 3,
|
||||
receiptNumber: 'REC-2024-003-GHI',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
describe('status', () => {
|
||||
it('should return status from helper function using linkedSignal', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
const expectedStatus = ReceiptCompleteStatus.Abgeschlossen;
|
||||
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(expectedStatus);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.status()).toBe(expectedStatus);
|
||||
expect(getReceiptStatusFromReturn).toHaveBeenCalledWith(mockReturn);
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('24-001, 24-002, 24-003');
|
||||
});
|
||||
it('should update when input changes', () => {
|
||||
const mockReturn1 = createMockReturn({ id: 1 });
|
||||
const mockReturn2 = createMockReturn({ id: 2 });
|
||||
|
||||
it('should handle receipts with null data', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: null as any,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
// Setup first status
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn1);
|
||||
fixture.detectChanges();
|
||||
expect(component.status()).toBe(ReceiptCompleteStatus.Offen);
|
||||
|
||||
// Change status for second return
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Abgeschlossen,
|
||||
);
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn2);
|
||||
fixture.detectChanges();
|
||||
expect(component.status()).toBe(ReceiptCompleteStatus.Abgeschlossen);
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('24-002');
|
||||
});
|
||||
|
||||
it('should handle receipts with undefined receiptNumber', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: undefined as any,
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('24-002');
|
||||
});
|
||||
|
||||
it('should handle short receipt numbers', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'SHORT',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemQuantity computed signal', () => {
|
||||
it('should return 0 when no receipts', () => {
|
||||
const mockReturn = createMockReturn({ receipts: [] });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(0);
|
||||
describe('Template rendering', () => {
|
||||
beforeEach(() => {
|
||||
// Setup default mock values for template rendering tests
|
||||
vi.mocked(getReceiptNumberFromReturn).mockReturnValue('24-001');
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(3);
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return sum of all items across receipts', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: new Array(5),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: new Array(3),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
data: {
|
||||
id: 3,
|
||||
receiptNumber: 'REC-2024-003-GHI',
|
||||
items: new Array(7),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
it('should display receipt information correctly', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(15);
|
||||
const compiled = fixture.nativeElement;
|
||||
|
||||
// Check receipt number display
|
||||
expect(compiled.textContent).toContain('#24-001');
|
||||
|
||||
// Check item quantity display
|
||||
expect(compiled.textContent).toContain('3');
|
||||
|
||||
// Check status display
|
||||
expect(compiled.textContent).toContain('Offen');
|
||||
});
|
||||
|
||||
it('should handle receipts with null data', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: null as any,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
items: new Array(3),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
it('should apply correct background class for "Offen" status', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(3);
|
||||
const containerDiv = fixture.nativeElement.querySelector(
|
||||
'.flex.flex-col.gap-6',
|
||||
);
|
||||
expect(containerDiv.classList.contains('bg-isa-white')).toBe(true);
|
||||
expect(containerDiv.classList.contains('bg-isa-neutral-400')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle receipts with undefined items', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: undefined as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: new Array(5),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
it('should apply correct background class for "Abgeschlossen" status', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Abgeschlossen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(5);
|
||||
const containerDiv = fixture.nativeElement.querySelector(
|
||||
'.flex.flex-col.gap-6',
|
||||
);
|
||||
expect(containerDiv.classList.contains('bg-isa-neutral-400')).toBe(true);
|
||||
expect(containerDiv.classList.contains('bg-isa-white')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle receipts with empty items array', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: new Array(2),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
it('should show action component for "Offen" status', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(2);
|
||||
const actionsComponent = fixture.nativeElement.querySelector(
|
||||
'lib-remission-return-receipt-actions',
|
||||
);
|
||||
expect(actionsComponent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should hide action component for "Abgeschlossen" status', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Abgeschlossen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const actionsComponent = fixture.nativeElement.querySelector(
|
||||
'lib-remission-return-receipt-actions',
|
||||
);
|
||||
expect(actionsComponent).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should pass correct inputs to action component', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const actionsComponent = fixture.nativeElement.querySelector(
|
||||
'lib-remission-return-receipt-actions',
|
||||
);
|
||||
expect(actionsComponent).toBeTruthy();
|
||||
|
||||
// Since we're using MockComponent, we can verify the component is present
|
||||
// and that it should receive the remissionReturn input based on the template
|
||||
// The actual input binding testing would require a more complex setup
|
||||
expect(actionsComponent.tagName.toLowerCase()).toBe(
|
||||
'lib-remission-return-receipt-actions',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completed computed signal', () => {
|
||||
it('should return "Offen" when no receipts are completed', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
describe('Output events', () => {
|
||||
it('should emit reloadList when action component emits reloadData', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.completed()).toBe('Offen');
|
||||
});
|
||||
const reloadListSpy = vi.fn();
|
||||
component.reloadList.subscribe(reloadListSpy);
|
||||
|
||||
it('should return "Abgeschlossen" when at least one receipt is completed', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
data: {
|
||||
id: 3,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
// Simulate the action component emitting reloadData
|
||||
const actionsComponent = fixture.nativeElement.querySelector(
|
||||
'lib-remission-return-receipt-actions',
|
||||
);
|
||||
|
||||
// Trigger the reloadData event from the actions component
|
||||
actionsComponent.dispatchEvent(new CustomEvent('reloadData'));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.completed()).toBe('Abgeschlossen');
|
||||
});
|
||||
|
||||
it('should return "Abgeschlossen" when all receipts are completed', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.completed()).toBe('Abgeschlossen');
|
||||
});
|
||||
|
||||
it('should return "Offen" when no receipts exist', () => {
|
||||
const mockReturn = createMockReturn({ receipts: [] });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.completed()).toBe('Offen');
|
||||
});
|
||||
|
||||
it('should handle receipts with null data', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: null as any,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.completed()).toBe('Offen');
|
||||
});
|
||||
|
||||
it('should handle receipts with undefined completed status', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: [],
|
||||
completed: undefined as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: [],
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.completed()).toBe('Abgeschlossen');
|
||||
expect(reloadListSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component reactivity', () => {
|
||||
it('should update computed signals when input changes', () => {
|
||||
const initialReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-001-ABC',
|
||||
items: new Array(3),
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const initialReturn = createMockReturn({ id: 1 });
|
||||
const updatedReturn = createMockReturn({ id: 2 });
|
||||
|
||||
// Mock return values for initial state
|
||||
vi.mocked(getReceiptNumberFromReturn).mockReturnValue('24-001');
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(3);
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Offen,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', initialReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('24-001');
|
||||
expect(component.itemQuantity()).toBe(3);
|
||||
expect(component.completed()).toBe('Offen');
|
||||
expect(component.status()).toBe(ReceiptCompleteStatus.Offen);
|
||||
|
||||
// Mock return values for updated state
|
||||
vi.mocked(getReceiptNumberFromReturn).mockReturnValue('24-002, 24-003');
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(7);
|
||||
vi.mocked(getReceiptStatusFromReturn).mockReturnValue(
|
||||
ReceiptCompleteStatus.Abgeschlossen,
|
||||
);
|
||||
|
||||
const updatedReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber: 'REC-2024-002-DEF',
|
||||
items: new Array(5),
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: 'REC-2024-003-GHI',
|
||||
items: new Array(2),
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', updatedReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('24-002, 24-003');
|
||||
expect(component.itemQuantity()).toBe(7);
|
||||
expect(component.completed()).toBe('Abgeschlossen');
|
||||
expect(component.status()).toBe(ReceiptCompleteStatus.Abgeschlossen);
|
||||
});
|
||||
|
||||
it('should trigger helper functions with correct parameters when input changes', () => {
|
||||
const mockReturn1 = createMockReturn({ id: 1 });
|
||||
const mockReturn2 = createMockReturn({ id: 2 });
|
||||
|
||||
// First input
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getReceiptNumberFromReturn).toHaveBeenCalledWith(mockReturn1);
|
||||
expect(getReceiptItemQuantityFromReturn).toHaveBeenCalledWith(
|
||||
mockReturn1,
|
||||
);
|
||||
expect(getReceiptStatusFromReturn).toHaveBeenCalledWith(mockReturn1);
|
||||
|
||||
// Clear mock calls
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Second input
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn2);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getReceiptNumberFromReturn).toHaveBeenCalledWith(mockReturn2);
|
||||
expect(getReceiptItemQuantityFromReturn).toHaveBeenCalledWith(
|
||||
mockReturn2,
|
||||
);
|
||||
expect(getReceiptStatusFromReturn).toHaveBeenCalledWith(mockReturn2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('status alias', () => {
|
||||
it('should have status as an alias for completed', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.status()).toBe(component.completed());
|
||||
expect(component.status()).toBe('Abgeschlossen');
|
||||
});
|
||||
|
||||
it('should update status when completed changes', () => {
|
||||
const initialReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', initialReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.status()).toBe('Offen');
|
||||
|
||||
const updatedReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
completed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', updatedReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.status()).toBe('Abgeschlossen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle return with deeply nested null values', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: null as any,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: {
|
||||
id: 2,
|
||||
receiptNumber: null as any,
|
||||
items: null as any,
|
||||
completed: null as any,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('');
|
||||
expect(component.itemQuantity()).toBe(0);
|
||||
expect(component.completed()).toBe('Offen');
|
||||
});
|
||||
|
||||
it('should handle very long receipt numbers', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [
|
||||
{
|
||||
id: 1,
|
||||
data: {
|
||||
id: 1,
|
||||
receiptNumber:
|
||||
'PREFIX-VERY-LONG-RECEIPT-NUMBER-THAT-EXCEEDS-NORMAL-LENGTH',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptNumber()).toBe('-VERY-');
|
||||
});
|
||||
|
||||
it('should handle large number of receipts', () => {
|
||||
const receipts = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
data: {
|
||||
id: i + 1,
|
||||
receiptNumber: `REC-2024-${String(i + 1).padStart(3, '0')}-ABC`,
|
||||
items: new Array(2),
|
||||
completed: i % 2 === 0 ? '2024-01-15T10:30:00.000Z' : undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
const mockReturn = createMockReturn({ receipts });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(200);
|
||||
expect(component.completed()).toBe('Abgeschlossen');
|
||||
expect(component.receiptNumber()).toContain('24-001');
|
||||
expect(component.receiptNumber()).toContain('24-100');
|
||||
describe('Constants', () => {
|
||||
it('should expose ReceiptCompleteStatus constant', () => {
|
||||
expect(component.ReceiptCompleteStatus).toBe(ReceiptCompleteStatus);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,22 +3,29 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
linkedSignal,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { Return } from '@isa/remission/data-access';
|
||||
import {
|
||||
Return,
|
||||
ReceiptCompleteStatusValue,
|
||||
ReceiptCompleteStatus,
|
||||
getReceiptStatusFromReturn,
|
||||
getReceiptItemQuantityFromReturn,
|
||||
getReceiptNumberFromReturn,
|
||||
} from '@isa/remission/data-access';
|
||||
import { RemissionReturnReceiptActionsComponent } from '@isa/remission/shared/return-receipt-actions';
|
||||
|
||||
/**
|
||||
* Component that displays a single return receipt item in the list view.
|
||||
* Shows receipt number, item quantity, and status information.
|
||||
* Component that displays a list item for a remission return receipt.
|
||||
* Shows the receipt number, item quantity, and status of the return.
|
||||
*
|
||||
* @component
|
||||
* @selector remi-return-receipt-list-item
|
||||
* @standalone
|
||||
*
|
||||
* @example
|
||||
* <remi-return-receipt-list-item
|
||||
* [remissionReturn]="returnData"
|
||||
* [returnReceipt]="receiptData">
|
||||
* </remi-return-receipt-list-item>
|
||||
* <remi-return-receipt-list-item [remissionReturn]="returnData"></remi-return-receipt-list-item>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-return-receipt-list-item',
|
||||
@@ -26,81 +33,54 @@ import { Return } from '@isa/remission/data-access';
|
||||
styleUrls: ['./return-receipt-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [],
|
||||
imports: [RemissionReturnReceiptActionsComponent],
|
||||
host: {
|
||||
'class': 'remi-return-receipt-list-item',
|
||||
'[class.remi-return-receipt-list-item--offen]': 'completed() === "Offen"',
|
||||
'[class.remi-return-receipt-list-item--abgeschlossen]':
|
||||
'completed() === "Abgeschlossen"',
|
||||
class: 'remi-return-receipt-list-item',
|
||||
},
|
||||
})
|
||||
export class ReturnReceiptListItemComponent {
|
||||
/** Constant for receipt completion status */
|
||||
ReceiptCompleteStatus = ReceiptCompleteStatus;
|
||||
|
||||
/**
|
||||
* Required input for the return data.
|
||||
* Input for the remission return data.
|
||||
* @input
|
||||
* @required
|
||||
*/
|
||||
remissionReturn = input.required<Return>();
|
||||
|
||||
/**
|
||||
* Computed signal that extracts and formats receipt numbers from all receipts.
|
||||
* Returns "Keine Belege vorhanden" if no receipts, otherwise returns formatted receipt numbers.
|
||||
* @returns {string} The formatted receipt numbers or message
|
||||
* Output event that emits when the list needs to be reloaded.
|
||||
* @output
|
||||
*/
|
||||
reloadList = output<void>();
|
||||
|
||||
/**
|
||||
* Computed signal that retrieves the receipt number from the return data.
|
||||
* Uses the helper function to get the receipt number.
|
||||
* @returns {string} The receipt number from the return
|
||||
*/
|
||||
receiptNumber = computed(() => {
|
||||
const returnData = this.remissionReturn();
|
||||
|
||||
if (!returnData.receipts || returnData.receipts.length === 0) {
|
||||
return 'Keine Belege vorhanden';
|
||||
}
|
||||
|
||||
const receiptNumbers = returnData.receipts
|
||||
.map((receipt) => receipt.data?.receiptNumber)
|
||||
.filter((receiptNumber) => receiptNumber && receiptNumber.length >= 12)
|
||||
.map((receiptNumber) => receiptNumber!.substring(6, 12));
|
||||
|
||||
return receiptNumbers.length > 0 ? receiptNumbers.join(', ') : '';
|
||||
return getReceiptNumberFromReturn(returnData);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that calculates the total quantity of all items across all receipts.
|
||||
* @returns {number} Total quantity of items
|
||||
* Computed signal that calculates the total item quantity from all receipts in the return.
|
||||
* Uses the helper function to get the quantity.
|
||||
* @returns {number} The total item quantity from all receipts
|
||||
*/
|
||||
itemQuantity = computed(() => {
|
||||
const returnData = this.remissionReturn();
|
||||
|
||||
if (!returnData.receipts || returnData.receipts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return returnData.receipts.reduce((totalItems, receipt) => {
|
||||
const items = receipt.data?.items;
|
||||
return totalItems + (items ? items.length : 0);
|
||||
}, 0);
|
||||
return getReceiptItemQuantityFromReturn(returnData);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that determines the completion status.
|
||||
* Returns "Abgeschlossen" if any receipt is completed, "Offen" otherwise.
|
||||
* @returns {'Abgeschlossen' | 'Offen'} Status text based on completion state
|
||||
* Linked signal that determines the completion status of the return.
|
||||
* Uses the helper function to get the status based on the return data.
|
||||
* @returns {ReceiptCompleteStatusValue} The completion status of the return
|
||||
*/
|
||||
completed = computed(() => {
|
||||
status = linkedSignal<ReceiptCompleteStatusValue>(() => {
|
||||
const returnData = this.remissionReturn();
|
||||
|
||||
if (!returnData.receipts || returnData.receipts.length === 0) {
|
||||
return 'Offen';
|
||||
}
|
||||
|
||||
const hasCompletedReceipt = returnData.receipts.some(
|
||||
(receipt) => receipt.data?.completed,
|
||||
);
|
||||
|
||||
return hasCompletedReceipt ? 'Abgeschlossen' : 'Offen';
|
||||
return getReceiptStatusFromReturn(returnData);
|
||||
});
|
||||
|
||||
/**
|
||||
* Alias for completed for backward compatibility with tests.
|
||||
* @deprecated Use completed() instead
|
||||
*/
|
||||
status = this.completed;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './lib/remission-start-dialog/remission-start-dialog.component';
|
||||
export * from './lib/remission-start-dialog/remission-start.service';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import './test-mocks'; // Import mocks before anything else
|
||||
import './test-mocks';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
RemissionStartDialogComponent,
|
||||
@@ -63,6 +63,10 @@ describe('RemissionStartDialogComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(RemissionStartDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// Manually set the dialog data since it's not being injected properly
|
||||
(component as any).data = mockDialogData;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -135,7 +139,7 @@ describe('RemissionStartDialogComponent', () => {
|
||||
|
||||
it('should close dialog when receipt type is close', async () => {
|
||||
// Arrange
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
const closeSpy = vi.spyOn(component, 'onDialogClose');
|
||||
const returnReceipt = { type: ReturnReceiptResultType.Close } as const;
|
||||
|
||||
// Act
|
||||
@@ -155,7 +159,7 @@ describe('RemissionStartDialogComponent', () => {
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
const closeSpy = vi.spyOn(component, 'onDialogClose');
|
||||
|
||||
// Act
|
||||
await component.onAssignPackageNumber(packageNumber);
|
||||
@@ -170,12 +174,11 @@ describe('RemissionStartDialogComponent', () => {
|
||||
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');
|
||||
const closeSpy = vi.spyOn(component, 'onDialogClose');
|
||||
component.assignPackageStepData.set({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
@@ -191,7 +194,7 @@ describe('RemissionStartDialogComponent', () => {
|
||||
|
||||
it('should close dialog when no step data available', async () => {
|
||||
// Arrange
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
const closeSpy = vi.spyOn(component, 'onDialogClose');
|
||||
component.assignPackageStepData.set(undefined);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import './test-mocks';
|
||||
import { vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { RemissionStartService } from './remission-start.service';
|
||||
import { RemissionStore } from '@isa/remission/data-access';
|
||||
import { injectDialog } from '@isa/ui/dialog';
|
||||
|
||||
describe('RemissionStartService', () => {
|
||||
let service: RemissionStartService;
|
||||
let mockRemissionStore: any;
|
||||
let mockDialog: any;
|
||||
let mockDialogRef: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock objects
|
||||
mockRemissionStore = {
|
||||
startRemission: vi.fn(),
|
||||
};
|
||||
|
||||
mockDialogRef = {
|
||||
closed: of({ returnId: 'test-return-id', receiptId: 'test-receipt-id' }),
|
||||
};
|
||||
|
||||
mockDialog = vi.fn().mockReturnValue(mockDialogRef);
|
||||
|
||||
// Mock the injectDialog function to return our mock dialog
|
||||
vi.mocked(injectDialog).mockReturnValue(mockDialog);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
RemissionStartService,
|
||||
{ provide: RemissionStore, useValue: mockRemissionStore },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(RemissionStartService);
|
||||
});
|
||||
|
||||
it('should start remission successfully when dialog returns result', async () => {
|
||||
// Arrange
|
||||
const returnGroup = 'test-return-group';
|
||||
|
||||
// Act
|
||||
await service.startRemission(returnGroup);
|
||||
|
||||
// Assert
|
||||
expect(mockDialog).toHaveBeenCalledWith({
|
||||
data: { returnGroup },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.startRemission).toHaveBeenCalledWith({
|
||||
returnId: 'test-return-id',
|
||||
receiptId: 'test-receipt-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { injectDialog } from '@isa/ui/dialog';
|
||||
import { RemissionStartDialogComponent } from './remission-start-dialog.component';
|
||||
import { RemissionStore } from '@isa/remission/data-access';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RemissionStartService {
|
||||
#remissionStartDialog = injectDialog(RemissionStartDialogComponent);
|
||||
#remissionStore = inject(RemissionStore);
|
||||
|
||||
async startRemission(returnGroup: string | undefined) {
|
||||
const remissionStartDialogRef = this.#remissionStartDialog({
|
||||
data: { returnGroup },
|
||||
classList: ['gap-0'],
|
||||
width: '30rem',
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(remissionStartDialogRef.closed);
|
||||
|
||||
if (result) {
|
||||
const { returnId, receiptId } = result;
|
||||
this.#remissionStore.startRemission({
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,24 @@ export class MockScannerButtonComponent {
|
||||
scanResult = output<string>();
|
||||
}
|
||||
|
||||
// Mock ScannerService
|
||||
export class MockScannerService {
|
||||
scanBarcode = vi.fn().mockResolvedValue('mock-barcode-result');
|
||||
configure = vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
// Mock the entire scanner module
|
||||
vi.mock('@isa/shared/scanner', () => ({
|
||||
ScannerButtonComponent: MockScannerButtonComponent,
|
||||
ScannerService: MockScannerService,
|
||||
}));
|
||||
|
||||
// Mock the dialog injection function
|
||||
vi.mock('@isa/ui/dialog', () => ({
|
||||
injectDialog: vi.fn(),
|
||||
DialogContentDirective: class MockDialogContentDirective {
|
||||
close = vi.fn();
|
||||
data: any;
|
||||
},
|
||||
DialogComponent: class MockDialogComponent {},
|
||||
}));
|
||||
7
libs/remission/shared/return-receipt-actions/README.md
Normal file
7
libs/remission/shared/return-receipt-actions/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# remission-return-receipt-actions
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test remission-return-receipt-actions` to execute the unit tests.
|
||||
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/remission/shared/return-receipt-actions/project.json
Normal file
20
libs/remission/shared/return-receipt-actions/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "remission-return-receipt-actions",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/remission/shared/return-receipt-actions/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../../coverage/libs/remission/shared/return-receipt-actions"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './lib/remission-return-receipt-actions/remission-return-receipt-actions.component';
|
||||
export * from './lib/remission-return-receipt-actions/remission-return-receipt-complete.component';
|
||||
@@ -0,0 +1,30 @@
|
||||
<div
|
||||
class="flex flex-row gap-4"
|
||||
[class.justify-end]="displayDeleteAction()"
|
||||
[class.justify-center]="!displayDeleteAction()"
|
||||
data-which="return-receipt-actions"
|
||||
>
|
||||
@if (displayDeleteAction()) {
|
||||
<button
|
||||
uiButton
|
||||
type="button"
|
||||
color="secondary"
|
||||
(click)="onDelete(); $event.stopPropagation(); $event.preventDefault()"
|
||||
[disabled]="itemQuantity() > 0"
|
||||
data-what="return-receipt-delete-button"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
uiButton
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="
|
||||
onContinueRemission(); $event.stopPropagation(); $event.preventDefault()
|
||||
"
|
||||
data-what="return-receipt-continue-button"
|
||||
>
|
||||
Befüllen
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,761 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Router } from '@angular/router';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { RemissionReturnReceiptActionsComponent } from './remission-return-receipt-actions.component';
|
||||
import {
|
||||
Return,
|
||||
getReceiptItemQuantityFromReturn,
|
||||
RemissionStore,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { provideLoggerContext } from '@isa/core/logging';
|
||||
import { injectConfirmationDialog } from '@isa/ui/dialog';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
|
||||
// Mock the helper functions and services
|
||||
vi.mock('@isa/remission/data-access', async () => {
|
||||
const actual = await vi.importActual('@isa/remission/data-access');
|
||||
return {
|
||||
...actual,
|
||||
getReceiptItemQuantityFromReturn: vi.fn(),
|
||||
RemissionStore: vi.fn(),
|
||||
RemissionReturnReceiptService: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@angular/router', () => ({
|
||||
Router: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the dialog injection function
|
||||
vi.mock('@isa/ui/dialog', () => ({
|
||||
injectConfirmationDialog: vi.fn(),
|
||||
DialogContentDirective: class MockDialogContentDirective {
|
||||
close = vi.fn();
|
||||
data: any;
|
||||
},
|
||||
DialogComponent: class MockDialogComponent {},
|
||||
}));
|
||||
|
||||
// Mock the tab injection function
|
||||
vi.mock('@isa/core/tabs', () => ({
|
||||
injectTabId: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('RemissionReturnReceiptActionsComponent', () => {
|
||||
let component: RemissionReturnReceiptActionsComponent;
|
||||
let fixture: ComponentFixture<RemissionReturnReceiptActionsComponent>;
|
||||
let mockRemissionStore: any;
|
||||
let mockRemissionReturnReceiptService: any;
|
||||
let mockRouter: any;
|
||||
let mockDialogRef: any;
|
||||
let mockInjectConfirmationDialog: any;
|
||||
let mockInjectTabId: any;
|
||||
|
||||
const createMockReturn = (overrides: Partial<Return> = {}): Return =>
|
||||
({
|
||||
id: 1,
|
||||
receipts: [
|
||||
{ id: 101, items: [] },
|
||||
{ id: 102, items: [] },
|
||||
],
|
||||
...overrides,
|
||||
}) as Return;
|
||||
|
||||
const createMockReceipt = (id: number, items: any[] = []) => ({
|
||||
id,
|
||||
items,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Create mock services
|
||||
mockRemissionStore = {
|
||||
receiptId: vi.fn().mockReturnValue(101),
|
||||
remissionStarted: vi.fn().mockReturnValue(false),
|
||||
isCurrentRemission: vi.fn().mockReturnValue(false),
|
||||
startRemission: vi.fn(),
|
||||
clearState: vi.fn(),
|
||||
};
|
||||
|
||||
mockRemissionReturnReceiptService = {
|
||||
cancelReturnReceipt: vi.fn().mockResolvedValue({}),
|
||||
cancelReturn: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
|
||||
mockRouter = {
|
||||
navigate: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
mockDialogRef = {
|
||||
closed: new Subject(),
|
||||
};
|
||||
|
||||
// Mock the injected functions
|
||||
mockInjectConfirmationDialog = vi.fn().mockReturnValue(mockDialogRef);
|
||||
mockInjectTabId = vi.fn().mockReturnValue('test-tab-id');
|
||||
|
||||
// Setup the mocks for the import functions
|
||||
vi.mocked(injectConfirmationDialog).mockReturnValue(
|
||||
mockInjectConfirmationDialog,
|
||||
);
|
||||
vi.mocked(injectTabId).mockReturnValue(mockInjectTabId);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RemissionReturnReceiptActionsComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RemissionStore, useValue: mockRemissionStore },
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
provideLoggerContext({ component: 'Test' }),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemissionReturnReceiptActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have remissionReturn as required input', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.remissionReturn()).toBeDefined();
|
||||
expect(component.remissionReturn()).toEqual(mockReturn);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed signals', () => {
|
||||
describe('returnId', () => {
|
||||
it('should return the ID from the remission return', () => {
|
||||
const mockReturn = createMockReturn({ id: 123 });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.returnId()).toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiptIds', () => {
|
||||
it('should return array of receipt IDs', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [createMockReceipt(101), createMockReceipt(102)],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptIds()).toEqual([101, 102]);
|
||||
});
|
||||
|
||||
it('should return empty array when no receipts', () => {
|
||||
const mockReturn = createMockReturn({ receipts: undefined });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.receiptIds()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('firstReceiptId', () => {
|
||||
it('should return first receipt ID when receipts exist', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [createMockReceipt(101), createMockReceipt(102)],
|
||||
});
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.firstReceiptId()).toBe(101);
|
||||
});
|
||||
|
||||
it('should return undefined when no receipts exist', () => {
|
||||
const mockReturn = createMockReturn({ receipts: [] });
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.firstReceiptId()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemQuantity', () => {
|
||||
it('should return item quantity from helper function', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
const expectedQuantity = 5;
|
||||
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(
|
||||
expectedQuantity,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.itemQuantity()).toBe(expectedQuantity);
|
||||
expect(getReceiptItemQuantityFromReturn).toHaveBeenCalledWith(
|
||||
mockReturn,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCurrentRemission', () => {
|
||||
it('should return true when return is current remission', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
mockRemissionStore.receiptId.mockReturnValue(101);
|
||||
mockRemissionStore.isCurrentRemission.mockReturnValue(true);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.isCurrentRemission()).toBe(true);
|
||||
expect(mockRemissionStore.isCurrentRemission).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 101,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when receipt is not found in current receipts', () => {
|
||||
const mockReturn = createMockReturn({
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
mockRemissionStore.receiptId.mockReturnValue(999); // Different receipt ID
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.isCurrentRemission()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template rendering', () => {
|
||||
beforeEach(() => {
|
||||
// Setup default mock values for template rendering tests
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(3);
|
||||
});
|
||||
|
||||
it('should render delete button', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const deleteButton = fixture.nativeElement.querySelector(
|
||||
'button[color="secondary"]',
|
||||
);
|
||||
expect(deleteButton).toBeTruthy();
|
||||
expect(deleteButton.textContent.trim()).toBe('Löschen');
|
||||
});
|
||||
|
||||
it('should render continue button', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const continueButton = fixture.nativeElement.querySelector(
|
||||
'button[color="primary"]',
|
||||
);
|
||||
expect(continueButton).toBeTruthy();
|
||||
expect(continueButton.textContent.trim()).toBe('Befüllen');
|
||||
});
|
||||
|
||||
it('should have correct data attributes for E2E testing', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const container = fixture.nativeElement.querySelector(
|
||||
'[data-which="return-receipt-actions"]',
|
||||
);
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
const deleteButton = fixture.nativeElement.querySelector(
|
||||
'[data-what="return-receipt-delete-button"]',
|
||||
);
|
||||
expect(deleteButton).toBeTruthy();
|
||||
|
||||
const continueButton = fixture.nativeElement.querySelector(
|
||||
'[data-what="return-receipt-continue-button"]',
|
||||
);
|
||||
expect(continueButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should disable delete button when itemQuantity is greater than 0', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(5);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const deleteButton = fixture.nativeElement.querySelector(
|
||||
'button[color="secondary"]',
|
||||
);
|
||||
expect(deleteButton.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should enable delete button when itemQuantity is 0', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(0);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const deleteButton = fixture.nativeElement.querySelector(
|
||||
'button[color="secondary"]',
|
||||
);
|
||||
expect(deleteButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should never disable continue button', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(5);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const continueButton = fixture.nativeElement.querySelector(
|
||||
'button[color="primary"]',
|
||||
);
|
||||
expect(continueButton.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component methods', () => {
|
||||
describe('onDelete', () => {
|
||||
it('should call service methods and emit reloadData when deleting return', async () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101), createMockReceipt(102)],
|
||||
});
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Mock the isCurrentRemission method to avoid the signal error
|
||||
const isCurrentRemissionSpy = vi
|
||||
.spyOn(component, 'isCurrentRemission')
|
||||
.mockReturnValue(false);
|
||||
|
||||
await component.onDelete();
|
||||
|
||||
// Should cancel each receipt
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.cancelReturnReceipt,
|
||||
).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.cancelReturnReceipt,
|
||||
).toHaveBeenNthCalledWith(1, {
|
||||
returnId: 123,
|
||||
receiptId: 101,
|
||||
});
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.cancelReturnReceipt,
|
||||
).toHaveBeenNthCalledWith(2, {
|
||||
returnId: 123,
|
||||
receiptId: 102,
|
||||
});
|
||||
|
||||
// Should cancel the return
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.cancelReturn,
|
||||
).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
});
|
||||
|
||||
// Should emit reload data
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
|
||||
isCurrentRemissionSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should clear state when deleting current remission', async () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
|
||||
// Set up the component to think this is the current remission
|
||||
const isCurrentRemissionSpy = vi
|
||||
.spyOn(component, 'isCurrentRemission')
|
||||
.mockReturnValue(true);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.onDelete();
|
||||
|
||||
expect(mockRemissionStore.clearState).toHaveBeenCalled();
|
||||
|
||||
isCurrentRemissionSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle errors during deletion', async () => {
|
||||
const mockReturn = createMockReturn();
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const error = new Error('Network error');
|
||||
|
||||
// Mock the isCurrentRemission method to avoid the signal error
|
||||
const isCurrentRemissionSpy = vi
|
||||
.spyOn(component, 'isCurrentRemission')
|
||||
.mockReturnValue(false);
|
||||
|
||||
mockRemissionReturnReceiptService.cancelReturnReceipt.mockRejectedValue(
|
||||
error,
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
await component.onDelete();
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error deleting return receipt:',
|
||||
error,
|
||||
);
|
||||
expect(reloadDataSpy).toHaveBeenCalled(); // Should still emit even on error
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
isCurrentRemissionSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onContinueRemission', () => {
|
||||
it('should start remission and navigate when no remission is started', async () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
mockRemissionStore.remissionStarted.mockReturnValue(false);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.onContinueRemission();
|
||||
|
||||
expect(mockRemissionStore.startRemission).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 101,
|
||||
});
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not start remission when no first receipt ID exists', async () => {
|
||||
const mockReturn = createMockReturn({ receipts: [] });
|
||||
mockRemissionStore.remissionStarted.mockReturnValue(false);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.onContinueRemission();
|
||||
|
||||
expect(mockRemissionStore.startRemission).not.toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should show confirmation dialog when remission is started but not current', async () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
mockRemissionStore.remissionStarted.mockReturnValue(true);
|
||||
|
||||
// Mock isCurrentRemission to return false
|
||||
const isCurrentRemissionSpy = vi
|
||||
.spyOn(component, 'isCurrentRemission')
|
||||
.mockReturnValue(false);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Mock dialog confirmation result
|
||||
const dialogResultSubject = new Subject();
|
||||
mockDialogRef.closed = dialogResultSubject.asObservable();
|
||||
|
||||
const continuePromise = component.onContinueRemission();
|
||||
|
||||
// Simulate user confirming the dialog
|
||||
dialogResultSubject.next({ confirmed: true });
|
||||
dialogResultSubject.complete();
|
||||
|
||||
await continuePromise;
|
||||
|
||||
expect(mockInjectConfirmationDialog).toHaveBeenCalledWith({
|
||||
title: 'Bereits geöffneter Warenbegleitschein',
|
||||
width: '30rem',
|
||||
data: {
|
||||
message:
|
||||
'Möchten Sie wirklich einen neuen öffnen, oder möchten Sie zuerst den aktuellen abschließen?',
|
||||
closeText: 'Aktuellen bearbeiten',
|
||||
confirmText: 'Neuen öffnen',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.clearState).toHaveBeenCalled();
|
||||
expect(mockRemissionStore.startRemission).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 101,
|
||||
});
|
||||
|
||||
isCurrentRemissionSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not start new remission when dialog is cancelled', async () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
mockRemissionStore.remissionStarted.mockReturnValue(true);
|
||||
|
||||
// Mock isCurrentRemission to return false
|
||||
const isCurrentRemissionSpy = vi
|
||||
.spyOn(component, 'isCurrentRemission')
|
||||
.mockReturnValue(false);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Mock dialog cancellation result
|
||||
const dialogResultSubject = new Subject();
|
||||
mockDialogRef.closed = dialogResultSubject.asObservable();
|
||||
|
||||
const continuePromise = component.onContinueRemission();
|
||||
|
||||
// Simulate user cancelling the dialog
|
||||
dialogResultSubject.next({ confirmed: false });
|
||||
dialogResultSubject.complete();
|
||||
|
||||
await continuePromise;
|
||||
|
||||
expect(mockRemissionStore.clearState).not.toHaveBeenCalled();
|
||||
expect(mockRemissionStore.startRemission).not.toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
|
||||
isCurrentRemissionSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should navigate directly when current remission matches', async () => {
|
||||
const mockReturn = createMockReturn({
|
||||
id: 123,
|
||||
receipts: [createMockReceipt(101)],
|
||||
});
|
||||
mockRemissionStore.remissionStarted.mockReturnValue(true);
|
||||
|
||||
// Mock isCurrentRemission to return true
|
||||
const isCurrentRemissionSpy = vi
|
||||
.spyOn(component, 'isCurrentRemission')
|
||||
.mockReturnValue(true);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.onContinueRemission();
|
||||
|
||||
expect(mockInjectConfirmationDialog).not.toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
|
||||
isCurrentRemissionSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToRemissionList', () => {
|
||||
it('should navigate to correct remission route', async () => {
|
||||
await component.navigateToRemissionList();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component reactivity', () => {
|
||||
it('should update computed signals when input changes', () => {
|
||||
const initialReturn = createMockReturn({ id: 1 });
|
||||
const updatedReturn = createMockReturn({ id: 2 });
|
||||
|
||||
// Mock return values for initial state
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(3);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', initialReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.returnId()).toBe(1);
|
||||
expect(component.itemQuantity()).toBe(3);
|
||||
|
||||
// Mock return values for updated state
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(7);
|
||||
|
||||
fixture.componentRef.setInput('remissionReturn', updatedReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.returnId()).toBe(2);
|
||||
expect(component.itemQuantity()).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User interactions', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getReceiptItemQuantityFromReturn).mockReturnValue(0);
|
||||
});
|
||||
|
||||
it('should call onDelete when delete button is clicked', async () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const onDeleteSpy = vi.spyOn(component, 'onDelete').mockResolvedValue();
|
||||
|
||||
const deleteButton = fixture.nativeElement.querySelector(
|
||||
'button[color="secondary"]',
|
||||
);
|
||||
deleteButton.click();
|
||||
|
||||
expect(onDeleteSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onContinueRemission when continue button is clicked', async () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const onContinueRemissionSpy = vi
|
||||
.spyOn(component, 'onContinueRemission')
|
||||
.mockResolvedValue();
|
||||
|
||||
const continueButton = fixture.nativeElement.querySelector(
|
||||
'button[color="primary"]',
|
||||
);
|
||||
continueButton.click();
|
||||
|
||||
expect(onContinueRemissionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent event propagation and default behavior on delete click', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
vi.spyOn(component, 'onDelete').mockResolvedValue();
|
||||
|
||||
const deleteButton = fixture.nativeElement.querySelector(
|
||||
'button[color="secondary"]',
|
||||
);
|
||||
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation');
|
||||
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
|
||||
|
||||
deleteButton.dispatchEvent(event);
|
||||
|
||||
expect(stopPropagationSpy).toHaveBeenCalled();
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent event propagation and default behavior on continue click', () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
vi.spyOn(component, 'onContinueRemission').mockResolvedValue();
|
||||
|
||||
const continueButton = fixture.nativeElement.querySelector(
|
||||
'button[color="primary"]',
|
||||
);
|
||||
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation');
|
||||
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
|
||||
|
||||
continueButton.dispatchEvent(event);
|
||||
|
||||
expect(stopPropagationSpy).toHaveBeenCalled();
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output events', () => {
|
||||
it('should emit reloadData on successful deletion', async () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
await component.onDelete();
|
||||
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit reloadData even on deletion error', async () => {
|
||||
const mockReturn = createMockReturn();
|
||||
fixture.componentRef.setInput('remissionReturn', mockReturn);
|
||||
fixture.detectChanges();
|
||||
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
mockRemissionReturnReceiptService.cancelReturnReceipt.mockRejectedValue(
|
||||
new Error('Network error'),
|
||||
);
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
await component.onDelete();
|
||||
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Return,
|
||||
getReceiptItemQuantityFromReturn,
|
||||
RemissionStore,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { Router } from '@angular/router';
|
||||
import { injectConfirmationDialog } from '@isa/ui/dialog';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-remission-return-receipt-actions',
|
||||
templateUrl: './remission-return-receipt-actions.component.html',
|
||||
styleUrl: './remission-return-receipt-actions.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ButtonComponent],
|
||||
})
|
||||
export class RemissionReturnReceiptActionsComponent {
|
||||
/**
|
||||
* Remission store for managing remission state.
|
||||
*/
|
||||
#remissionStore = inject(RemissionStore);
|
||||
|
||||
/**
|
||||
* Service for handling remission return receipt operations.
|
||||
*/
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/** Angular Router for navigation */
|
||||
router = inject(Router);
|
||||
|
||||
/**
|
||||
* Dialog service for confirmation prompts.
|
||||
*/
|
||||
#confirmDialog = injectConfirmationDialog();
|
||||
|
||||
/**
|
||||
* Injects the current activated tab ID as a signal.
|
||||
* This is used to determine if the current remission matches the active tab.
|
||||
*/
|
||||
activatedTabId = injectTabId();
|
||||
|
||||
/** Logger for this component */
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionReturnReceiptActionsComponent',
|
||||
}));
|
||||
|
||||
/** displayDeleteAction - Determines if the delete action should be displayed */
|
||||
displayDeleteAction = input<boolean>(true);
|
||||
|
||||
/**
|
||||
* Input for the remission return data.
|
||||
* @input
|
||||
*/
|
||||
remissionReturn = input.required<Return>();
|
||||
|
||||
/**
|
||||
* Output event that emits when the list needs to be reloaded.
|
||||
* @output
|
||||
*/
|
||||
reloadData = output<void>();
|
||||
|
||||
/**
|
||||
* Computed signal that retrieves the return ID from the remission return data.
|
||||
* This is used to identify the specific return for operations like deletion or continuation.
|
||||
* @returns {number} The ID of the remission return
|
||||
*/
|
||||
returnId = computed(() => this.remissionReturn().id);
|
||||
|
||||
/**
|
||||
* Computed signal that retrieves all receipt IDs associated with the remission return.
|
||||
* This is used to manage multiple receipts within a single return.
|
||||
* @returns {Array<number>} Array of receipt IDs
|
||||
*/
|
||||
receiptIds = computed(() => {
|
||||
return this.remissionReturn().receipts?.map((r) => r.id) || [];
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that retrieves the first receipt ID from the remission return.
|
||||
* This is useful for operations that require a single receipt ID, such as starting a remission.
|
||||
* @returns {number | undefined} The first receipt ID or undefined if no receipts exist
|
||||
*/
|
||||
firstReceiptId = computed(() => {
|
||||
const receiptIds = this.receiptIds();
|
||||
return receiptIds.length > 0 ? receiptIds[0] : undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that calculates the total item quantity from all receipts in the return.
|
||||
* Uses the helper function to get the quantity.
|
||||
* @returns {number} The total item quantity from all receipts
|
||||
*/
|
||||
itemQuantity = computed(() => {
|
||||
const returnData = this.remissionReturn();
|
||||
return getReceiptItemQuantityFromReturn(returnData);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that checks if the current remission is the one being displayed.
|
||||
* This is used to determine if the return is currently active in the remission flow.
|
||||
* @returns {boolean} True if the current remission matches, false otherwise
|
||||
*/
|
||||
isCurrentRemission = computed(() => {
|
||||
const returnId = this.returnId();
|
||||
const receiptIds = this.receiptIds();
|
||||
const currentReceiptId = this.#remissionStore?.receiptId();
|
||||
const currentlyOpenReceiptId = receiptIds?.find(
|
||||
(id) => currentReceiptId === id,
|
||||
);
|
||||
|
||||
if (!currentlyOpenReceiptId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.#remissionStore.isCurrentRemission({
|
||||
returnId,
|
||||
receiptId: currentlyOpenReceiptId,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Method to delete the return receipt.
|
||||
* This method handles the deletion of the return and its associated receipts.
|
||||
* It checks if the return is the current remission and clears the store state if necessary.
|
||||
* Emits the reloadList event after successful deletion to refresh the list.
|
||||
* @returns {Promise<void>} A promise that resolves when the deletion is complete
|
||||
*/
|
||||
async onDelete(): Promise<void> {
|
||||
try {
|
||||
const returnId = this.returnId();
|
||||
const receiptIds = this.receiptIds();
|
||||
|
||||
for (const receiptId of receiptIds) {
|
||||
if (this.isCurrentRemission()) {
|
||||
this.#remissionStore.clearState();
|
||||
}
|
||||
|
||||
await this.#remissionReturnReceiptService.cancelReturnReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.#remissionReturnReceiptService.cancelReturn({
|
||||
returnId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.#logger.error('Error deleting return receipt', error);
|
||||
console.error('Error deleting return receipt:', error);
|
||||
}
|
||||
|
||||
this.reloadData.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to continue with the remission process.
|
||||
* If the remission is already started, it navigates to the remission list.
|
||||
* If not, it checks if there is a first receipt ID and starts the remission with it.
|
||||
* If a remission is already in progress, it prompts the user to either continue with the current remission or start a new one.
|
||||
* @returns {Promise<void>} A promise that resolves when navigation is complete
|
||||
*/
|
||||
async onContinueRemission() {
|
||||
if (!this.#remissionStore.remissionStarted()) {
|
||||
const firstReceiptId = this.firstReceiptId();
|
||||
if (firstReceiptId) {
|
||||
this.#remissionStore.startRemission({
|
||||
returnId: this.returnId(),
|
||||
receiptId: firstReceiptId,
|
||||
});
|
||||
|
||||
return await this.navigateToRemissionList();
|
||||
}
|
||||
} else if (!this.isCurrentRemission()) {
|
||||
const dialogRef = this.#confirmDialog({
|
||||
title: 'Bereits geöffneter Warenbegleitschein',
|
||||
width: '30rem',
|
||||
data: {
|
||||
message:
|
||||
'Möchten Sie wirklich einen neuen öffnen, oder möchten Sie zuerst den aktuellen abschließen?',
|
||||
closeText: 'Aktuellen bearbeiten',
|
||||
confirmText: 'Neuen öffnen',
|
||||
},
|
||||
});
|
||||
const dialogResult = await firstValueFrom(dialogRef.closed);
|
||||
// Neuen WBS öffnen
|
||||
if (dialogResult?.confirmed) {
|
||||
this.#remissionStore.clearState(); // Alte Remission im Store zurücksetzen
|
||||
const firstReceiptId = this.firstReceiptId();
|
||||
if (firstReceiptId) {
|
||||
this.#remissionStore.startRemission({
|
||||
returnId: this.returnId(),
|
||||
receiptId: firstReceiptId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.navigateToRemissionList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the remission list based on the current activated tab ID.
|
||||
* This method is used to redirect the user to the remission list after completing or starting a remission.
|
||||
* @returns {Promise<void>} A promise that resolves when navigation is complete
|
||||
*/
|
||||
async navigateToRemissionList() {
|
||||
await this.router.navigate(['/', this.activatedTabId(), 'remission']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<button
|
||||
[disabled]="completingRemission() || itemsLength() === 0"
|
||||
[pending]="completingRemission()"
|
||||
color="brand"
|
||||
size="large"
|
||||
class="fixed right-6 bottom-6"
|
||||
uiButton
|
||||
type="button"
|
||||
(click)="completeRemission()"
|
||||
>
|
||||
Wanne abschließen
|
||||
</button>
|
||||
@@ -0,0 +1,221 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { RemissionReturnReceiptCompleteComponent } from './remission-return-receipt-complete.component';
|
||||
import {
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
} from '@isa/remission/data-access';
|
||||
import { RemissionStartService } from '@isa/remission/shared/remission-start-dialog';
|
||||
import { provideLoggerContext } from '@isa/core/logging';
|
||||
|
||||
// Mock the services
|
||||
vi.mock('@isa/remission/data-access', async () => {
|
||||
const actual = await vi.importActual('@isa/remission/data-access');
|
||||
return {
|
||||
...actual,
|
||||
RemissionReturnReceiptService: vi.fn(),
|
||||
RemissionStore: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@isa/remission/shared/remission-start-dialog', () => ({
|
||||
RemissionStartService: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@angular/router', () => ({
|
||||
Router: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the dialog injection function
|
||||
vi.mock('@isa/ui/dialog', () => ({
|
||||
injectConfirmationDialog: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the tab injection function
|
||||
vi.mock('@isa/core/tabs', () => ({
|
||||
injectTabId: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('RemissionReturnReceiptCompleteComponent', () => {
|
||||
let component: RemissionReturnReceiptCompleteComponent;
|
||||
let fixture: ComponentFixture<RemissionReturnReceiptCompleteComponent>;
|
||||
let mockRemissionReturnReceiptService: any;
|
||||
let mockRemissionStartService: any;
|
||||
let mockRouter: any;
|
||||
let mockRemissionStore: any;
|
||||
let mockDialogRef: any;
|
||||
let mockInjectConfirmationDialog: any;
|
||||
let mockInjectTabId: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Create mock services
|
||||
mockRemissionReturnReceiptService = {
|
||||
completeReturnReceiptAndReturn: vi.fn(),
|
||||
completeReturnGroup: vi.fn(),
|
||||
};
|
||||
|
||||
mockRemissionStartService = {
|
||||
startRemission: vi.fn(),
|
||||
};
|
||||
|
||||
mockRouter = {
|
||||
navigate: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
mockRemissionStore = {
|
||||
clearState: vi.fn(),
|
||||
};
|
||||
|
||||
mockDialogRef = {
|
||||
closed: new Subject(),
|
||||
};
|
||||
|
||||
// Mock the injected functions
|
||||
mockInjectConfirmationDialog = vi.fn().mockReturnValue(mockDialogRef);
|
||||
mockInjectTabId = vi.fn().mockReturnValue('test-tab-id');
|
||||
|
||||
// Setup the mocks for the import functions
|
||||
const { injectConfirmationDialog } = await import('@isa/ui/dialog');
|
||||
const { injectTabId } = await import('@isa/core/tabs');
|
||||
vi.mocked(injectConfirmationDialog).mockReturnValue(
|
||||
mockInjectConfirmationDialog,
|
||||
);
|
||||
vi.mocked(injectTabId).mockReturnValue(mockInjectTabId);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RemissionReturnReceiptCompleteComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionReturnReceiptService,
|
||||
},
|
||||
{ provide: RemissionStartService, useValue: mockRemissionStartService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: RemissionStore, useValue: mockRemissionStore },
|
||||
provideLoggerContext({ component: 'Test' }),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemissionReturnReceiptCompleteComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// Set required inputs
|
||||
fixture.componentRef.setInput('returnId', 123);
|
||||
fixture.componentRef.setInput('receiptId', 456);
|
||||
fixture.componentRef.setInput('itemsLength', 5);
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('Component Setup', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have correct initial values', () => {
|
||||
expect(component.returnId()).toBe(123);
|
||||
expect(component.receiptId()).toBe(456);
|
||||
expect(component.itemsLength()).toBe(5);
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component methods', () => {
|
||||
describe('completeSingleReturnReceipt', () => {
|
||||
it('should complete single return receipt successfully', async () => {
|
||||
// Arrange
|
||||
const mockReturn = { id: 123, returnGroup: 'RG001' };
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await component.completeSingleReturnReceipt();
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(mockRemissionStore.clearState).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockReturn);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeRemission', () => {
|
||||
it('should complete remission without return group', async () => {
|
||||
// Arrange
|
||||
const mockReturn = { id: 123, returnGroup: null };
|
||||
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Act
|
||||
await component.completeRemission();
|
||||
|
||||
// Assert
|
||||
expect(component.completingRemission()).toBe(false);
|
||||
expect(mockInjectConfirmationDialog).not.toHaveBeenCalled();
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent multiple completion attempts', async () => {
|
||||
// Arrange
|
||||
component.completingRemission.set(true);
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Act
|
||||
await component.completeRemission();
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(reloadDataSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit reloadData on successful completion', async () => {
|
||||
// Arrange
|
||||
const mockReturn = { id: 123, returnGroup: null };
|
||||
mockRemissionReturnReceiptService.completeReturnReceiptAndReturn.mockResolvedValue(
|
||||
mockReturn,
|
||||
);
|
||||
|
||||
const reloadDataSpy = vi.fn();
|
||||
component.reloadData.subscribe(reloadDataSpy);
|
||||
|
||||
// Act
|
||||
await component.completeRemission();
|
||||
|
||||
// Assert
|
||||
expect(reloadDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToRemissionList', () => {
|
||||
it('should navigate to remission list', async () => {
|
||||
// Act
|
||||
await component.navigateToRemissionList();
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([
|
||||
'/',
|
||||
'test-tab-id',
|
||||
'remission',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import {
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
Return,
|
||||
} from '@isa/remission/data-access';
|
||||
import { RemissionStartService } from '@isa/remission/shared/remission-start-dialog';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectConfirmationDialog } from '@isa/ui/dialog';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-remission-return-receipt-complete',
|
||||
templateUrl: './remission-return-receipt-complete.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ButtonComponent],
|
||||
})
|
||||
export class RemissionReturnReceiptCompleteComponent {
|
||||
/** Logger for this component */
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionReturnReceiptCompleteComponent',
|
||||
}));
|
||||
|
||||
/** Service for managing remission return receipt operations */
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/** Service for starting a new remission */
|
||||
#remissionStartService = inject(RemissionStartService);
|
||||
|
||||
/** Angular Router for navigation */
|
||||
router = inject(Router);
|
||||
|
||||
/**
|
||||
* Injects a confirmation dialog service for user interactions.
|
||||
* This dialog is used to confirm actions like completing a return.
|
||||
* The dialog can be customized with title and data options.
|
||||
* @example
|
||||
* const dialogRef = this.#dialog({
|
||||
* title: 'Wanne abgeschlossen',
|
||||
* data: {
|
||||
* message: 'Legen Sie abschließend den “Blank Beizettel” in die abgeschlossene Wanne.',
|
||||
* closeText: 'Neue Wanne',
|
||||
* confirmText: 'Beenden',
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
#confirmDialog = injectConfirmationDialog();
|
||||
|
||||
/**
|
||||
* Injects the current activated tab ID as a signal.
|
||||
* This is used to determine if the current remission matches the active tab.
|
||||
*/
|
||||
activatedTabId = injectTabId();
|
||||
|
||||
/** Instance of the RemissionStore for managing remission state */
|
||||
store = inject(RemissionStore);
|
||||
|
||||
/**
|
||||
* Required input for the return ID.
|
||||
* Automatically coerced to a number from string input.
|
||||
* @input
|
||||
* @required
|
||||
*/
|
||||
returnId = input.required<number>();
|
||||
|
||||
/**
|
||||
* Required input for the receipt ID.
|
||||
* Automatically coerced to a number from string input.
|
||||
* @input
|
||||
* @required
|
||||
*/
|
||||
receiptId = input.required<number>();
|
||||
|
||||
/**
|
||||
* Required input for the number of items in the return.
|
||||
* This is used to display the count of items in the return receipt.
|
||||
* @input
|
||||
* @required
|
||||
*/
|
||||
itemsLength = input.required<number>();
|
||||
|
||||
/**
|
||||
* Output event that emits when the list needs to be reloaded.
|
||||
* This is used to refresh the remission list after completing a return.
|
||||
* @output
|
||||
*/
|
||||
reloadData = output<void>();
|
||||
|
||||
/**
|
||||
* Signal to track if the remission is currently being completed.
|
||||
* This prevents multiple submissions while a completion operation is in progress.
|
||||
*/
|
||||
completingRemission = signal(false);
|
||||
|
||||
/**
|
||||
* Completes the current remission return receipt.
|
||||
* This method handles the completion logic, including confirmation dialogs and navigation.
|
||||
* It also emits a reload event to refresh the remission list.
|
||||
* @returns {Promise<Return>} The completed return data
|
||||
*/
|
||||
async completeSingleReturnReceipt(): Promise<Return> {
|
||||
const completedReturn =
|
||||
await this.#remissionReturnReceiptService.completeReturnReceiptAndReturn({
|
||||
returnId: this.returnId(),
|
||||
receiptId: this.receiptId(),
|
||||
});
|
||||
this.store.clearState();
|
||||
return completedReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the completion of the remission return receipt.
|
||||
* It checks if the remission is already being completed to avoid duplicate actions.
|
||||
* If not, it proceeds to complete the remission and handle the post-completion logic.
|
||||
*/
|
||||
async completeRemission() {
|
||||
if (this.completingRemission()) {
|
||||
return;
|
||||
}
|
||||
this.completingRemission.set(true);
|
||||
try {
|
||||
const completedReturn = await this.completeSingleReturnReceipt();
|
||||
const returnGroup = completedReturn?.returnGroup;
|
||||
|
||||
if (returnGroup) {
|
||||
const dialogRef = this.#confirmDialog({
|
||||
title: 'Wanne abgeschlossen',
|
||||
width: '30rem',
|
||||
data: {
|
||||
message:
|
||||
'Legen Sie abschließend den “Blank Beizettel” in die abgeschlossene Wanne. Der Warenbegleitschein wird nach Abschluss der Remission versendet. Zum Öffnen eines neuen Warenbegleitscheins setzen Sie die Remission fort.',
|
||||
closeText: 'Neue Wanne',
|
||||
confirmText: 'Beenden',
|
||||
},
|
||||
});
|
||||
const dialogResult = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (dialogResult?.confirmed) {
|
||||
// Beenden - Remission abschließen Flow
|
||||
await this.#remissionReturnReceiptService.completeReturnGroup({
|
||||
returnGroup,
|
||||
});
|
||||
} else {
|
||||
// Starten - Neue Wanne Flow
|
||||
await this.#remissionStartService.startRemission(returnGroup);
|
||||
await this.navigateToRemissionList();
|
||||
}
|
||||
}
|
||||
|
||||
this.reloadData.emit();
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to complete remission', error);
|
||||
}
|
||||
this.completingRemission.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the remission list.
|
||||
*/
|
||||
async navigateToRemissionList() {
|
||||
await this.router.navigate(['/', this.activatedTabId(), 'remission']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { vi } from 'vitest';
|
||||
import { Component, output } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
// Mock ScannerButtonComponent
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
@Component({
|
||||
selector: 'isa-scanner-button',
|
||||
template: '<button>Scan</button>',
|
||||
standalone: true,
|
||||
})
|
||||
export class MockScannerButtonComponent {
|
||||
scanResult = output<string>();
|
||||
}
|
||||
|
||||
// Mock ScannerService
|
||||
export class MockScannerService {
|
||||
scanBarcode = vi.fn().mockResolvedValue('mock-barcode-result');
|
||||
configure = vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
// Mock the entire scanner module
|
||||
vi.mock('@isa/shared/scanner', () => ({
|
||||
ScannerButtonComponent: MockScannerButtonComponent,
|
||||
ScannerService: MockScannerService,
|
||||
}));
|
||||
|
||||
// Mock the dialog injection function
|
||||
vi.mock('@isa/ui/dialog', () => ({
|
||||
injectDialog: vi.fn(),
|
||||
injectConfirmationDialog: vi.fn(() =>
|
||||
vi.fn(() => ({
|
||||
closed: of({ confirmed: false }),
|
||||
})),
|
||||
),
|
||||
DialogContentDirective: class MockDialogContentDirective {
|
||||
close = vi.fn();
|
||||
data: any;
|
||||
},
|
||||
DialogComponent: class MockDialogComponent {},
|
||||
}));
|
||||
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/remission/shared/return-receipt-actions/tsconfig.json
Normal file
30
libs/remission/shared/return-receipt-actions/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
29
libs/remission/shared/return-receipt-actions/vite.config.mts
Normal file
29
libs/remission/shared/return-receipt-actions/vite.config.mts
Normal file
@@ -0,0 +1,29 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir:
|
||||
'../../../../node_modules/.vite/libs/remission/shared/return-receipt-actions',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory:
|
||||
'../../../../coverage/libs/remission/shared/return-receipt-actions',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,17 +1,24 @@
|
||||
<div class="w-full flex flex-row justify-between items-start">
|
||||
<filter-search-bar-input
|
||||
class="flex flex-row gap-4 h-12"
|
||||
[appearance]="'results'"
|
||||
inputKey="qs"
|
||||
(triggerSearch)="triggerSearch.emit($event)"
|
||||
data-what="search-input"
|
||||
></filter-search-bar-input>
|
||||
<div
|
||||
class="w-full flex flex-row justify-between items-start"
|
||||
[class.empty-filter-input]="!hasFilter() && !hasInput()"
|
||||
>
|
||||
@if (hasInput()) {
|
||||
<filter-search-bar-input
|
||||
class="flex flex-row gap-4 h-12"
|
||||
[appearance]="'results'"
|
||||
[inputKey]="inputKey()"
|
||||
(triggerSearch)="triggerSearch.emit($event)"
|
||||
data-what="search-input"
|
||||
></filter-search-bar-input>
|
||||
}
|
||||
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<filter-filter-menu-button
|
||||
(applied)="triggerSearch.emit('filter')"
|
||||
[rollbackOnClose]="true"
|
||||
></filter-filter-menu-button>
|
||||
@if (hasFilter()) {
|
||||
<filter-filter-menu-button
|
||||
(applied)="triggerSearch.emit('filter')"
|
||||
[rollbackOnClose]="true"
|
||||
></filter-filter-menu-button>
|
||||
}
|
||||
|
||||
@if (mobileBreakpoint()) {
|
||||
<ui-icon-button
|
||||
@@ -23,7 +30,9 @@
|
||||
></ui-icon-button>
|
||||
} @else {
|
||||
<filter-order-by-toolbar
|
||||
[class.empty-filter-input-width]="!hasFilter() && !hasInput()"
|
||||
(toggled)="triggerSearch.emit('order-by')"
|
||||
data-what="sort-toolbar"
|
||||
></filter-order-by-toolbar>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.filter-controls-panel {
|
||||
@apply flex flex-col gap-4;
|
||||
@apply w-full flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.empty-filter-input {
|
||||
@apply justify-end;
|
||||
}
|
||||
|
||||
.empty-filter-input-width {
|
||||
@apply w-[44.375rem];
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
linkedSignal,
|
||||
output,
|
||||
signal,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { SearchBarInputComponent } from '../inputs';
|
||||
import { FilterMenuButtonComponent } from '../menus/filter-menu';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionFilter, isaActionSort } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { OrderByToolbarComponent } from '../order-by';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { SearchTrigger } from '../types';
|
||||
import { InputType, SearchTrigger } from '../types';
|
||||
import { FilterService, TextFilterInput } from '../core';
|
||||
import { SearchBarInputComponent } from '../inputs';
|
||||
|
||||
/**
|
||||
* Filter controls panel component that provides a unified interface for search and filtering operations.
|
||||
@@ -53,6 +57,17 @@ import { SearchTrigger } from '../types';
|
||||
providers: [provideIcons({ isaActionSort, isaActionFilter })],
|
||||
})
|
||||
export class FilterControlsPanelComponent {
|
||||
/**
|
||||
* Service for managing filter state and operations.
|
||||
*/
|
||||
#filterService = inject(FilterService);
|
||||
|
||||
/**
|
||||
* The unique key identifier for this input in the filter system.
|
||||
* @default 'qs'
|
||||
*/
|
||||
inputKey = signal('qs');
|
||||
|
||||
/**
|
||||
* Output event that emits when any search action is triggered.
|
||||
* Provides the specific SearchTrigger type to indicate how the search was initiated:
|
||||
@@ -75,4 +90,26 @@ export class FilterControlsPanelComponent {
|
||||
* Linked to mobileBreakpoint to automatically adjust when screen size changes.
|
||||
*/
|
||||
showOrderByToolbarMobile = linkedSignal(() => !this.mobileBreakpoint());
|
||||
|
||||
/**
|
||||
* Computed signal that determines if the search input is present in the filter inputs.
|
||||
* This checks if there is a TextFilterInput with the specified inputKey.
|
||||
* Used to conditionally render the search input in the template.
|
||||
*/
|
||||
hasInput = computed(() => {
|
||||
const inputs = this.#filterService.inputs();
|
||||
const input = inputs.find(
|
||||
(input) => input.key === this.inputKey() && input.type === InputType.Text,
|
||||
) as TextFilterInput;
|
||||
return !!input;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal that checks if there are any active filters applied.
|
||||
* This is determined by checking if there are any inputs of types other than Text.
|
||||
*/
|
||||
hasFilter = computed(() => {
|
||||
const inputs = this.#filterService.inputs();
|
||||
return inputs.some((input) => input.type !== InputType.Text);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
injectMessageDialog,
|
||||
injectTextInputDialog,
|
||||
injectNumberInputDialog,
|
||||
injectConfirmationDialog,
|
||||
} from './injects';
|
||||
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
|
||||
import { DialogComponent } from './dialog.component';
|
||||
@@ -15,6 +16,7 @@ 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';
|
||||
import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
|
||||
|
||||
// Test component extending DialogContentDirective for testing
|
||||
@Component({ template: '' })
|
||||
@@ -218,4 +220,23 @@ describe('Dialog Injects', () => {
|
||||
expect(injector.get(DIALOG_CONTENT)).toBe(FeedbackDialogComponent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectConfirmationDialog', () => {
|
||||
it('should create a dialog injector for ConfirmationDialogComponent', () => {
|
||||
// Act
|
||||
const openConfirmationDialog = TestBed.runInInjectionContext(() =>
|
||||
injectConfirmationDialog(),
|
||||
);
|
||||
openConfirmationDialog({
|
||||
data: {
|
||||
message: 'Test message',
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const callOptions = mockDialogOpen.mock.calls[0][1];
|
||||
const injector = callOptions.injector;
|
||||
expect(injector.get(DIALOG_CONTENT)).toBe(ConfirmationDialogComponent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
FeedbackDialogComponent,
|
||||
FeedbackDialogData,
|
||||
} from './feedback-dialog/feedback-dialog.component';
|
||||
import {
|
||||
ConfirmationDialogComponent,
|
||||
ConfirmationDialogData,
|
||||
} from './confirmation-dialog/confirmation-dialog.component';
|
||||
|
||||
export interface InjectDialogOptions {
|
||||
/** Optional title override for the dialog */
|
||||
@@ -114,6 +118,15 @@ export function injectDialog<C extends DialogContentDirective<any, any>>(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function that returns a pre-configured ConfirmationDialog injector
|
||||
* @param options Optional configuration for the dialog
|
||||
* @returns A function to open a confirmation dialog
|
||||
*/
|
||||
export const injectConfirmationDialog = (
|
||||
options?: OpenDialogOptions<ConfirmationDialogData>,
|
||||
) => injectDialog(ConfirmationDialogComponent, options);
|
||||
|
||||
/**
|
||||
* Convenience function that returns a pre-configured MessageDialog injector
|
||||
* @returns A function to open a message dialog
|
||||
|
||||
57
package-lock.json
generated
57
package-lock.json
generated
@@ -1095,18 +1095,6 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/@types/node": {
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
|
||||
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -1336,6 +1324,7 @@
|
||||
"version": "20.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.1.2.tgz",
|
||||
"integrity": "sha512-NMSDavN+CJYvSze6wq7DpbrUA/EqiAD7GQoeJtuOknzUpPlWQmFOoHzTMKW+S34XlNEw+YQT0trv3DKcrE+T/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
@@ -11931,17 +11920,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
@@ -14679,6 +14657,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
@@ -15153,6 +15132,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@@ -16370,7 +16350,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
@@ -19082,7 +19062,7 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -27582,17 +27562,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -27632,6 +27601,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
@@ -27686,6 +27656,7 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
@@ -28667,7 +28638,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
@@ -29208,6 +29179,7 @@
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -31745,7 +31717,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -31805,15 +31777,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
"@isa/remission/shared/remission-start-dialog": [
|
||||
"libs/remission/shared/remission-start-dialog/src/index.ts"
|
||||
],
|
||||
"@isa/remission/shared/return-receipt-actions": [
|
||||
"libs/remission/shared/return-receipt-actions/src/index.ts"
|
||||
],
|
||||
"@isa/remission/shared/search-item-to-remit-dialog": [
|
||||
"libs/remission/shared/search-item-to-remit-dialog/src/index.ts"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user