mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
1 Commits
chore/pack
...
feature/52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb0103685b |
@@ -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