mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1905: feat(remission-data-access, remission-list-item): add remission item source t...
feat(remission-data-access, remission-list-item): add remission item source tracking and delete functionality Add comprehensive remission item source management with the ability to delete manually added items from return receipts. Introduces new RemissionItemSource model to track item origins and refactors remission list item components for better action management. Key changes: - Add RemissionItemSource model with 'manually-added' and 'DisposalListModule' types - Extend ReturnItem and ReturnSuggestion interfaces with source property - Implement deleteReturnItem service method with comprehensive error handling - Create RemissionListItemActionsComponent for managing item-specific actions - Add conditional display logic for delete buttons based on item source - Refactor RemissionListItemSelectComponent with hasStockToRemit input validation - Add deleteRemissionListItemInProgress state management across components - Include comprehensive test coverage for new delete functionality This enhancement enables users to remove manually added items from remission lists while preserving system-generated entries, improving workflow flexibility and data integrity. Ref: 5259
This commit is contained in:
@@ -15,3 +15,4 @@ export * from './supplier';
|
||||
export * from './receipt-return-tuple';
|
||||
export * from './receipt-return-suggestion-tuple';
|
||||
export * from './value-tuple-sting-and-integer';
|
||||
export * from './remission-item-source';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const RemissionItemSource = {
|
||||
ManuallyAdded: 'manually-added',
|
||||
DisposalListModule: 'DisposalListModule',
|
||||
} as const;
|
||||
|
||||
export type RemissionItemSourceKey = keyof typeof RemissionItemSource;
|
||||
export type RemissionItemSourceValue =
|
||||
(typeof RemissionItemSource)[RemissionItemSourceKey];
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ReturnItemDTO } from '@generated/swagger/inventory-api';
|
||||
import { Product } from './product';
|
||||
import { Price } from './price';
|
||||
import { RemissionItemSourceValue } from './remission-item-source';
|
||||
|
||||
export interface ReturnItem extends ReturnItemDTO {
|
||||
product: Product;
|
||||
retailPrice: Price;
|
||||
quantity: number;
|
||||
source: RemissionItemSourceValue;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ReturnSuggestionDTO } from '@generated/swagger/inventory-api';
|
||||
import { Product } from './product';
|
||||
import { Price } from './price';
|
||||
import { RemissionItemSourceValue } from './remission-item-source';
|
||||
|
||||
export interface ReturnSuggestion extends ReturnSuggestionDTO {
|
||||
product: Product;
|
||||
retailPrice: Price;
|
||||
quantity: number;
|
||||
source: RemissionItemSourceValue;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Return,
|
||||
Stock,
|
||||
Receipt,
|
||||
ReturnItem,
|
||||
RemissionListType,
|
||||
ReceiptReturnTuple,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
@@ -32,6 +33,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
ReturnCreateReceipt: jest.Mock;
|
||||
ReturnCreateAndAssignPackage: jest.Mock;
|
||||
ReturnRemoveReturnItem: jest.Mock;
|
||||
ReturnDeleteReturnItem: jest.Mock;
|
||||
ReturnFinalizeReceipt: jest.Mock;
|
||||
ReturnFinalizeReturn: jest.Mock;
|
||||
ReturnAddReturnItem: jest.Mock;
|
||||
@@ -91,6 +93,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
ReturnCreateReceipt: jest.fn(),
|
||||
ReturnCreateAndAssignPackage: jest.fn(),
|
||||
ReturnRemoveReturnItem: jest.fn(),
|
||||
ReturnDeleteReturnItem: jest.fn(),
|
||||
ReturnFinalizeReceipt: jest.fn(),
|
||||
ReturnFinalizeReturn: jest.fn(),
|
||||
ReturnAddReturnItem: jest.fn(),
|
||||
@@ -577,6 +580,95 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteReturnItem', () => {
|
||||
const mockReturnItem: ReturnItem = {
|
||||
id: 1001,
|
||||
quantity: 5,
|
||||
item: { id: 123, name: 'Test Item' },
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReturnService.ReturnDeleteReturnItem = jest.fn();
|
||||
});
|
||||
|
||||
it('should delete return item successfully', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnDeleteReturnItem.mockReturnValue(
|
||||
of({ result: mockReturnItem, error: null }),
|
||||
);
|
||||
|
||||
const params = { itemId: 1001 };
|
||||
|
||||
// Act
|
||||
const result = await service.deleteReturnItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockReturnItem);
|
||||
expect(mockReturnService.ReturnDeleteReturnItem).toHaveBeenCalledWith(
|
||||
params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
// Arrange
|
||||
const errorResponse = { error: 'API Error', result: null };
|
||||
mockReturnService.ReturnDeleteReturnItem.mockReturnValue(
|
||||
of(errorResponse),
|
||||
);
|
||||
|
||||
const params = { itemId: 1001 };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.deleteReturnItem(params)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnDeleteReturnItem.mockReturnValue(
|
||||
throwError(() => new Error('Observable error')),
|
||||
);
|
||||
|
||||
const params = { itemId: 1001 };
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.deleteReturnItem(params)).rejects.toThrow(
|
||||
'Observable error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return undefined when result is undefined', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnDeleteReturnItem.mockReturnValue(
|
||||
of({ error: null }),
|
||||
);
|
||||
|
||||
const params = { itemId: 1001 };
|
||||
|
||||
// Act
|
||||
const result = await service.deleteReturnItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return null when result is null', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnDeleteReturnItem.mockReturnValue(
|
||||
of({ result: null, error: null }),
|
||||
);
|
||||
|
||||
const params = { itemId: 1001 };
|
||||
|
||||
// Act
|
||||
const result = await service.deleteReturnItem(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeReturnReceipt', () => {
|
||||
const mockCompletedReceipt: Receipt = {
|
||||
id: 101,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReceiptReturnTuple,
|
||||
RemissionListType,
|
||||
ReturnItem,
|
||||
} from '../models';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { RemissionSupplierService } from './remission-supplier.service';
|
||||
@@ -407,6 +408,23 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteReturnItem(params: { itemId: number }) {
|
||||
this.#logger.debug('Deleting return item', () => ({ params }));
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnDeleteReturnItem(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to delete return item',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
return res?.result as ReturnItem;
|
||||
}
|
||||
|
||||
async completeReturnReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@if (displayRemoveManuallyAddedItemButton()) {
|
||||
<button
|
||||
class="self-end"
|
||||
type="button"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="deleteItemFromList()"
|
||||
[disabled]="deleteRemissionListItemInProgress()"
|
||||
[pending]="deleteRemissionListItemInProgress()"
|
||||
data-what="button"
|
||||
data-which="remove-remission-item"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (displayChangeQuantityButton()) {
|
||||
<button
|
||||
class="self-end"
|
||||
type="button"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="openRemissionQuantityDialog()"
|
||||
[disabled]="deleteRemissionListItemInProgress()"
|
||||
data-what="button"
|
||||
data-which="change-remission-quantity"
|
||||
>
|
||||
Remi Menge ändern
|
||||
</button>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply self-end flex flex-row h-full;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, Validators } from '@angular/forms';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import {
|
||||
RemissionItem,
|
||||
RemissionItemSource,
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
} from '@isa/remission/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectFeedbackDialog, injectNumberInputDialog } from '@isa/ui/dialog';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-list-item-actions',
|
||||
templateUrl: './remission-list-item-actions.component.html',
|
||||
styleUrl: './remission-list-item-actions.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, TextButtonComponent, CheckboxComponent],
|
||||
})
|
||||
export class RemissionListItemActionsComponent {
|
||||
/**
|
||||
* Dialog service for prompting the user to enter a remission quantity.
|
||||
* @private
|
||||
*/
|
||||
#dialog = injectNumberInputDialog();
|
||||
|
||||
/**
|
||||
* Dialog service for providing feedback to the user.
|
||||
* @private
|
||||
*/
|
||||
#feedbackDialog = injectFeedbackDialog();
|
||||
|
||||
/**
|
||||
* Logger instance for logging component events and errors.
|
||||
* @private
|
||||
*/
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionListItemActionsComponent',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Store for managing selected remission quantities.
|
||||
* @private
|
||||
*/
|
||||
#store = inject(RemissionStore);
|
||||
|
||||
/**
|
||||
* Service for handling remission return receipts.
|
||||
* @private
|
||||
*/
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/**
|
||||
* The item to display in the list.
|
||||
* Can be either a ReturnItem or a ReturnSuggestion.
|
||||
*/
|
||||
item = input.required<RemissionItem>();
|
||||
|
||||
/**
|
||||
* Signal indicating whether the item has stock to remit.
|
||||
* This is used to conditionally display the select component.
|
||||
*/
|
||||
hasStockToRemit = input.required<boolean>();
|
||||
|
||||
/**
|
||||
* ModelSignal indicating whether remission items are currently being processed.
|
||||
* Used to prevent multiple submissions or actions.
|
||||
* @default false
|
||||
*
|
||||
*/
|
||||
deleteRemissionListItemInProgress = model<boolean>();
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission has started.
|
||||
* Used to determine if the item can be selected or not.
|
||||
*/
|
||||
remissionStarted = computed(() => this.#store.remissionStarted());
|
||||
|
||||
/**
|
||||
* Computes whether to display the button for changing remission quantity.
|
||||
* Only displays if remission has started and there is stock to remit.
|
||||
*/
|
||||
displayChangeQuantityButton = computed(
|
||||
() => this.remissionStarted() && this.hasStockToRemit(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes whether to display the button for removing manually added items.
|
||||
* Only displays if the item's source is 'manually-added'.
|
||||
*/
|
||||
displayRemoveManuallyAddedItemButton = computed(
|
||||
() => this.item()?.source === RemissionItemSource.ManuallyAdded,
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens a dialog to change the remission quantity for the current item.
|
||||
* Prompts the user for a new quantity and updates the store if valid.
|
||||
* Displays feedback dialog upon successful update.
|
||||
*
|
||||
* @returns A promise that resolves when the dialog is closed.
|
||||
*/
|
||||
async openRemissionQuantityDialog(): Promise<void> {
|
||||
const dialogRef = this.#dialog({
|
||||
title: 'Remi-Menge ändern',
|
||||
data: {
|
||||
message: 'Wie viele Exemplare können remittiert werden?',
|
||||
inputLabel: 'Remi-Menge',
|
||||
inputValidation: [
|
||||
{
|
||||
errorKey: 'required',
|
||||
inputValidator: Validators.required,
|
||||
errorText: 'Bitte geben Sie eine Menge an.',
|
||||
},
|
||||
{
|
||||
errorKey: 'pattern',
|
||||
inputValidator: Validators.pattern(/^[1-9][0-9]*$/),
|
||||
errorText: 'Die Menge muss mindestens 1 sein.',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
const itemId = this.item()?.id;
|
||||
const quantity = result?.inputValue;
|
||||
|
||||
if (itemId && quantity !== undefined && quantity > 0) {
|
||||
this.#store.updateRemissionQuantity(itemId, this.item(), quantity);
|
||||
this.#feedbackDialog({
|
||||
data: { message: 'Remi-Menge wurde geändert' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the current item from the remission list.
|
||||
* Only proceeds if the item has an ID and deletion is not already in progress.
|
||||
* Sets the deleteRemissionListItemInProgress signal to true during deletion.
|
||||
* Logs an error if the deletion fails.
|
||||
*/
|
||||
async deleteItemFromList() {
|
||||
const itemId = this.item()?.id;
|
||||
if (!itemId || this.deleteRemissionListItemInProgress()) {
|
||||
return;
|
||||
}
|
||||
this.deleteRemissionListItemInProgress.set(true);
|
||||
|
||||
try {
|
||||
await this.#remissionReturnReceiptService.deleteReturnItem({ itemId });
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to delete return item', error);
|
||||
}
|
||||
|
||||
this.deleteRemissionListItemInProgress.set(false);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
<ui-checkbox appearance="bullet">
|
||||
<input
|
||||
type="checkbox"
|
||||
[ngModel]="itemSelected()"
|
||||
(ngModelChange)="setSelected($event)"
|
||||
(click)="$event.stopPropagation()"
|
||||
data-what="remission-item-selection-checkbox"
|
||||
[attr.data-which]="item()?.product?.ean"
|
||||
/>
|
||||
</ui-checkbox>
|
||||
@if (displayCheckbox()) {
|
||||
<ui-checkbox appearance="bullet">
|
||||
<input
|
||||
type="checkbox"
|
||||
[ngModel]="itemSelected()"
|
||||
(ngModelChange)="setSelected($event)"
|
||||
(click)="$event.stopPropagation()"
|
||||
data-what="remission-item-selection-checkbox"
|
||||
[attr.data-which]="item()?.product?.ean"
|
||||
/>
|
||||
</ui-checkbox>
|
||||
}
|
||||
|
||||
@@ -30,6 +30,26 @@ export class RemissionListItemSelectComponent {
|
||||
*/
|
||||
item = input.required<RemissionItem>();
|
||||
|
||||
/**
|
||||
* Signal indicating whether the item has stock to remit.
|
||||
* This is used to conditionally display the select component.
|
||||
*/
|
||||
hasStockToRemit = input.required<boolean>();
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission has started.
|
||||
* Used to determine if the item can be selected or not.
|
||||
*/
|
||||
remissionStarted = computed(() => this.#store.remissionStarted());
|
||||
|
||||
/**
|
||||
* Computes whether to display the checkbox for selecting the item.
|
||||
* Only displays if remission has started and there is stock to remit.
|
||||
*/
|
||||
displayCheckbox = computed(
|
||||
() => this.remissionStarted() && this.hasStockToRemit(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes whether the current item is selected in the remission store.
|
||||
* Checks if the item's ID exists in the selected items collection.
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
[item]="i"
|
||||
[orientation]="remiProductInfoOrientation()"
|
||||
></remi-product-info>
|
||||
@if (displayActions() && !desktopBreakpoint()) {
|
||||
@if (!desktopBreakpoint()) {
|
||||
<remi-feature-remission-list-item-select
|
||||
class="self-start mt-4"
|
||||
[item]="i"
|
||||
[hasStockToRemit]="hasStockToRemit()"
|
||||
></remi-feature-remission-list-item-select>
|
||||
}
|
||||
</ui-client-row-content>
|
||||
@@ -31,31 +32,22 @@
|
||||
></remi-product-stock-info>
|
||||
</ui-item-row-data>
|
||||
|
||||
@if (displayActions()) {
|
||||
<ui-item-row-data class="justify-end desktop:justify-between col-end-last">
|
||||
@if (desktopBreakpoint()) {
|
||||
<remi-feature-remission-list-item-select
|
||||
class="self-end mt-4"
|
||||
[item]="i"
|
||||
>
|
||||
</remi-feature-remission-list-item-select>
|
||||
}
|
||||
|
||||
<button
|
||||
class="self-end"
|
||||
type="button"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="
|
||||
openRemissionQuantityDialog();
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault()
|
||||
"
|
||||
data-what="button"
|
||||
data-which="change-remission-quantity"
|
||||
<ui-item-row-data class="justify-end desktop:justify-between col-end-last">
|
||||
@if (desktopBreakpoint()) {
|
||||
<remi-feature-remission-list-item-select
|
||||
class="self-end mt-4"
|
||||
[item]="i"
|
||||
[hasStockToRemit]="hasStockToRemit()"
|
||||
>
|
||||
Remi Menge ändern
|
||||
</button>
|
||||
</ui-item-row-data>
|
||||
}
|
||||
</remi-feature-remission-list-item-select>
|
||||
}
|
||||
|
||||
<remi-feature-remission-list-item-actions
|
||||
[item]="i"
|
||||
[hasStockToRemit]="hasStockToRemit()"
|
||||
(deleteRemissionListItemInProgressChange)="
|
||||
deleteRemissionListItemInProgress.set($event)
|
||||
"
|
||||
></remi-feature-remission-list-item-actions>
|
||||
</ui-item-row-data>
|
||||
</ui-client-row>
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductStockInfoComponent,
|
||||
ProductShelfMetaInfoComponent,
|
||||
} from '@isa/remission/shared/product';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
|
||||
import { RemissionListItemActionsComponent } from './remission-list-item-actions.component';
|
||||
import { signal } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
// --- Setup dynamic mocking for injectRemissionListType ---
|
||||
let remissionListTypeValue: RemissionListType = RemissionListType.Pflicht;
|
||||
@@ -34,20 +35,9 @@ jest.mock('@isa/remission/data-access', () => ({
|
||||
|
||||
// Mock the RemissionStore
|
||||
const mockRemissionStore = {
|
||||
remissionStarted: signal(true),
|
||||
selectedQuantity: signal({}),
|
||||
updateRemissionQuantity: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock the dialog services
|
||||
const mockNumberInputDialog = jest.fn();
|
||||
const mockFeedbackDialog = jest.fn();
|
||||
|
||||
jest.mock('@isa/ui/dialog', () => ({
|
||||
injectNumberInputDialog: () => mockNumberInputDialog,
|
||||
injectFeedbackDialog: () => mockFeedbackDialog,
|
||||
}));
|
||||
|
||||
describe('RemissionListItemComponent', () => {
|
||||
let component: RemissionListItemComponent;
|
||||
let fixture: ComponentFixture<RemissionListItemComponent>;
|
||||
@@ -92,7 +82,9 @@ describe('RemissionListItemComponent', () => {
|
||||
RemissionListItemComponent,
|
||||
MockComponent(ProductInfoComponent),
|
||||
MockComponent(ProductStockInfoComponent),
|
||||
MockComponent(ProductShelfMetaInfoComponent),
|
||||
MockComponent(RemissionListItemSelectComponent),
|
||||
MockComponent(RemissionListItemActionsComponent),
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
@@ -109,7 +101,6 @@ describe('RemissionListItemComponent', () => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
mockRemissionStore.selectedQuantity.set({});
|
||||
mockRemissionStore.remissionStarted.set(true);
|
||||
|
||||
// Reset the mocked functions to return default values
|
||||
const {
|
||||
@@ -142,6 +133,37 @@ describe('RemissionListItemComponent', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.stock()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have productGroupValue with default empty string', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
expect(component.productGroupValue()).toBe('');
|
||||
});
|
||||
|
||||
it('should accept productGroupValue input', () => {
|
||||
const testValue = 'Test Group';
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.componentRef.setInput('productGroupValue', testValue);
|
||||
fixture.detectChanges();
|
||||
expect(component.productGroupValue()).toBe(testValue);
|
||||
});
|
||||
|
||||
it('should have deleteRemissionListItemInProgress model with undefined default', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
expect(component.deleteRemissionListItemInProgress()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should accept deleteRemissionListItemInProgress model value', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.componentRef.setInput('deleteRemissionListItemInProgress', true);
|
||||
fixture.detectChanges();
|
||||
expect(component.deleteRemissionListItemInProgress()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed properties', () => {
|
||||
@@ -253,41 +275,49 @@ describe('RemissionListItemComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayActions', () => {
|
||||
it('should return true when stockToRemit > 0 and remission started', () => {
|
||||
describe('hasStockToRemit', () => {
|
||||
it('should return true when stockToRemit > 0', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(5);
|
||||
mockRemissionStore.remissionStarted.set(true);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayActions()).toBe(true);
|
||||
expect(component.hasStockToRemit()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when stockToRemit is 0', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(0);
|
||||
mockRemissionStore.remissionStarted.set(true);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayActions()).toBe(false);
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when remission has not started', () => {
|
||||
it('should return false when stockToRemit is negative', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(5);
|
||||
mockRemissionStore.remissionStarted.set(false);
|
||||
getStockToRemit.mockReturnValue(-1);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayActions()).toBe(false);
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remiProductInfoOrientation', () => {
|
||||
it('should return a valid orientation value', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
const orientation = component.remiProductInfoOrientation();
|
||||
expect(['horizontal', 'vertical']).toContain(orientation);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -331,58 +361,4 @@ describe('RemissionListItemComponent', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dialog interactions', () => {
|
||||
it('should open remission quantity dialog and update store on valid input', async () => {
|
||||
const mockDialogRef = {
|
||||
closed: of({ inputValue: 10 }), // Return Observable instead of object with toPromise
|
||||
};
|
||||
mockNumberInputDialog.mockReturnValue(mockDialogRef);
|
||||
|
||||
const mockItem = createMockReturnItem({ id: 1 });
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.openRemissionQuantityDialog();
|
||||
|
||||
expect(mockNumberInputDialog).toHaveBeenCalledWith({
|
||||
title: 'Remi-Menge ändern',
|
||||
data: {
|
||||
message: 'Wie viele Exemplare können remittiert werden?',
|
||||
inputLabel: 'Remi-Menge',
|
||||
inputValidation: expect.arrayContaining([
|
||||
expect.objectContaining({ errorKey: 'required' }),
|
||||
expect.objectContaining({ errorKey: 'pattern' }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockRemissionStore.updateRemissionQuantity).toHaveBeenCalledWith(
|
||||
1,
|
||||
mockItem,
|
||||
10,
|
||||
);
|
||||
expect(mockFeedbackDialog).toHaveBeenCalledWith({
|
||||
data: { message: 'Remi-Menge wurde geändert' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update store when dialog is cancelled', async () => {
|
||||
const mockDialogRef = {
|
||||
closed: of(null), // Return Observable with null result
|
||||
};
|
||||
mockNumberInputDialog.mockReturnValue(mockDialogRef);
|
||||
|
||||
const mockItem = createMockReturnItem({ id: 1 });
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
await component.openRemissionQuantityDialog();
|
||||
|
||||
expect(mockRemissionStore.updateRemissionQuantity).not.toHaveBeenCalled();
|
||||
expect(mockFeedbackDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, Validators } from '@angular/forms';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
calculateAvailableStock,
|
||||
calculateTargetStock,
|
||||
@@ -21,12 +22,11 @@ import {
|
||||
ProductStockInfoComponent,
|
||||
} from '@isa/remission/shared/product';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectFeedbackDialog, injectNumberInputDialog } from '@isa/ui/dialog';
|
||||
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { injectRemissionListType } from '../injects/inject-remission-list-type';
|
||||
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
|
||||
import { RemissionListItemActionsComponent } from './remission-list-item-actions.component';
|
||||
|
||||
/**
|
||||
* Component representing a single item in the remission list.
|
||||
@@ -56,21 +56,10 @@ import { RemissionListItemSelectComponent } from './remission-list-item-select.c
|
||||
ClientRowImports,
|
||||
ItemRowDataImports,
|
||||
RemissionListItemSelectComponent,
|
||||
RemissionListItemActionsComponent,
|
||||
],
|
||||
})
|
||||
export class RemissionListItemComponent {
|
||||
/**
|
||||
* Dialog service for prompting the user to enter a remission quantity.
|
||||
* @private
|
||||
*/
|
||||
#dialog = injectNumberInputDialog();
|
||||
|
||||
/**
|
||||
* Dialog service for providing feedback to the user.
|
||||
* @private
|
||||
*/
|
||||
#feedbackDialog = injectFeedbackDialog();
|
||||
|
||||
/**
|
||||
* Store for managing selected remission quantities.
|
||||
* @private
|
||||
@@ -99,6 +88,14 @@ export class RemissionListItemComponent {
|
||||
*/
|
||||
stock = input.required<StockInfo>();
|
||||
|
||||
/**
|
||||
* ModelSignal indicating whether remission items are currently being processed.
|
||||
* Used to prevent multiple submissions or actions.
|
||||
* @default false
|
||||
*
|
||||
*/
|
||||
deleteRemissionListItemInProgress = model<boolean>();
|
||||
|
||||
/**
|
||||
* Optional product group value for display or filtering.
|
||||
*/
|
||||
@@ -120,12 +117,10 @@ export class RemissionListItemComponent {
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes whether to display action buttons based on stock to remit and remission status.
|
||||
* @returns true if stock to remit is greater than 0 and remission has started
|
||||
* Computes whether the item has stock to remit.
|
||||
* Returns true if stockToRemit is greater than 0.
|
||||
*/
|
||||
displayActions = computed<boolean>(() => {
|
||||
return this.stockToRemit() > 0 && this.#store.remissionStarted();
|
||||
});
|
||||
hasStockToRemit = computed(() => this.stockToRemit() > 0);
|
||||
|
||||
/**
|
||||
* Computes the available stock for the item using stock and removedFromStock.
|
||||
@@ -169,44 +164,4 @@ export class RemissionListItemComponent {
|
||||
remainingQuantityInStock: this.remainingQuantityInStock(),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens a dialog to change the remission quantity for the current item.
|
||||
* Prompts the user for a new quantity and updates the store if valid.
|
||||
* Displays feedback dialog upon successful update.
|
||||
*
|
||||
* @returns A promise that resolves when the dialog is closed.
|
||||
*/
|
||||
async openRemissionQuantityDialog(): Promise<void> {
|
||||
const dialogRef = this.#dialog({
|
||||
title: 'Remi-Menge ändern',
|
||||
data: {
|
||||
message: 'Wie viele Exemplare können remittiert werden?',
|
||||
inputLabel: 'Remi-Menge',
|
||||
inputValidation: [
|
||||
{
|
||||
errorKey: 'required',
|
||||
inputValidator: Validators.required,
|
||||
errorText: 'Bitte geben Sie eine Menge an.',
|
||||
},
|
||||
{
|
||||
errorKey: 'pattern',
|
||||
inputValidator: Validators.pattern(/^[1-9][0-9]*$/),
|
||||
errorText: 'Die Menge muss mindestens 1 sein.',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
const itemId = this.item()?.id;
|
||||
const quantity = result?.inputValue;
|
||||
|
||||
if (itemId && quantity !== undefined && quantity > 0) {
|
||||
this.#store.updateRemissionQuantity(itemId, this.item(), quantity);
|
||||
this.#feedbackDialog({
|
||||
data: { message: 'Remi-Menge wurde geändert' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
[item]="item"
|
||||
[stock]="getStockForItem(item)"
|
||||
[productGroupValue]="getProductGroupValueForItem(item)"
|
||||
(deleteRemissionListItemInProgressChange)="
|
||||
onDeleteRemissionListItem($event)
|
||||
"
|
||||
></remi-feature-remission-list-item>
|
||||
} @placeholder {
|
||||
<div class="h-[7.75rem] w-full flex items-center justify-center">
|
||||
@@ -60,7 +63,7 @@
|
||||
size="large"
|
||||
color="brand"
|
||||
[pending]="remitItemsInProgress()"
|
||||
[disabled]="!hasSelectedItems()"
|
||||
[disabled]="!hasSelectedItems() || deleteRemissionListItemInProgress()"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
|
||||
@@ -149,6 +149,14 @@ export class RemissionListComponent {
|
||||
return this.selectedRemissionListType() === RemissionListType.Abteilung;
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission items are currently being processed.
|
||||
* Used to prevent multiple submissions or actions.
|
||||
* @default false
|
||||
*
|
||||
*/
|
||||
deleteRemissionListItemInProgress = signal(false);
|
||||
|
||||
/**
|
||||
* Resource signal for fetching the remission list based on current filters.
|
||||
* @returns Remission list resource state.
|
||||
@@ -310,6 +318,19 @@ export class RemissionListComponent {
|
||||
return productGroup ? productGroup.value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the deletion of a remission list item.
|
||||
* Updates the in-progress state and reloads the list and receipt upon completion.
|
||||
*
|
||||
* @param inProgress - Whether the deletion is currently in progress
|
||||
*/
|
||||
onDeleteRemissionListItem(inProgress: boolean) {
|
||||
this.deleteRemissionListItemInProgress.set(inProgress);
|
||||
if (!inProgress) {
|
||||
this.reloadListAndReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed signal that determines if the current search was triggered by user interaction.
|
||||
* Returns true for user-initiated actions (input, filter changes, sort changes, scanning)
|
||||
|
||||
Reference in New Issue
Block a user