Merged PR 1907: feat(remission-list-item, ui-dialog): enhance quantity dialog with original v...

feat(remission-list-item, ui-dialog): enhance quantity dialog with original value display

Add support for displaying original remission quantity in the quantity change dialog.
This provides better context for users when modifying remission quantities by showing
both the current input and the original calculated value.

Changes:
- Add subMessage and subMessageValue inputs to NumberInputComponent and dialog interfaces
- Update RemissionListItemActionsComponent to pass original quantity context to dialog
- Modify RemissionListItemComponent to track quantity differences and pass stockToRemit value
- Add selectedQuantityDiffersFromStockToRemit computed property for UI state management
- Update component templates to display contextual information in quantity dialogs

Ref: #5204
This commit is contained in:
Nino Righi
2025-08-06 15:58:10 +00:00
committed by Andreas Schickinger
parent 2dbf7dda37
commit 0dcb31973f
8 changed files with 185 additions and 23 deletions

View File

@@ -16,7 +16,6 @@ import {
} 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({
@@ -24,7 +23,7 @@ import { firstValueFrom } from 'rxjs';
templateUrl: './remission-list-item-actions.component.html',
styleUrl: './remission-list-item-actions.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [FormsModule, TextButtonComponent, CheckboxComponent],
imports: [FormsModule, TextButtonComponent],
})
export class RemissionListItemActionsComponent {
/**
@@ -66,10 +65,11 @@ export class RemissionListItemActionsComponent {
item = input.required<RemissionItem>();
/**
* Signal indicating whether the item has stock to remit.
* This is used to conditionally display the select component.
* The stock to remit for the current item.
* This is used to determine if the remission quantity can be changed.
* @default 0
*/
hasStockToRemit = input.required<boolean>();
stockToRemit = input.required<number>();
/**
* ModelSignal indicating whether remission items are currently being processed.
@@ -85,12 +85,18 @@ export class RemissionListItemActionsComponent {
*/
remissionStarted = computed(() => this.#store.remissionStarted());
/**
* Input signal indicating whether the selected quantity differs from the stock to remit.
* This is used to determine if the remission quantity can be changed.
*/
selectedQuantityDiffersFromStockToRemit = input<boolean>(true);
/**
* 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(),
() => this.remissionStarted() && this.stockToRemit() > 0,
);
/**
@@ -103,16 +109,21 @@ export class RemissionListItemActionsComponent {
/**
* 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.
* Prompts the user to enter a new quantity and validates the input.
* If the input is valid, updates the remission quantity in the store.
* Displays a feedback dialog upon successful update.
*/
async openRemissionQuantityDialog(): Promise<void> {
const dialogRef = this.#dialog({
title: 'Remi-Menge ändern',
data: {
message: 'Wie viele Exemplare können remittiert werden?',
subMessage: this.selectedQuantityDiffersFromStockToRemit()
? 'Originale Remi-Menge:'
: undefined,
subMessageValue: this.selectedQuantityDiffersFromStockToRemit()
? `${this.stockToRemit()}x`
: undefined,
inputLabel: 'Remi-Menge',
inputValidation: [
{

View File

@@ -44,7 +44,10 @@
<remi-feature-remission-list-item-actions
[item]="i"
[hasStockToRemit]="hasStockToRemit()"
[stockToRemit]="stockToRemit()"
[selectedQuantityDiffersFromStockToRemit]="
selectedQuantityDiffersFromStockToRemit()
"
(deleteRemissionListItemInProgressChange)="
deleteRemissionListItemInProgress.set($event)
"

View File

@@ -211,11 +211,19 @@ describe('RemissionListItemComponent', () => {
});
describe('targetStock', () => {
it('should calculate target stock correctly', () => {
const { calculateTargetStock } = require('@isa/remission/data-access');
it('should calculate target stock with remainingQuantityInStock when selected quantity matches stock to remit', () => {
const {
calculateTargetStock,
getStockToRemit,
} = require('@isa/remission/data-access');
calculateTargetStock.mockReturnValue(75);
getStockToRemit.mockReturnValue(25);
mockRemissionStore.selectedQuantity.set({ 1: 25 }); // Same as stockToRemit
const mockItem = createMockReturnItem({ remainingQuantityInStock: 15 });
const mockItem = createMockReturnItem({
id: 1,
remainingQuantityInStock: 15,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
@@ -223,7 +231,56 @@ describe('RemissionListItemComponent', () => {
expect(component.targetStock()).toBe(75);
expect(calculateTargetStock).toHaveBeenCalledWith({
availableStock: 100, // default mock value
stockToRemit: 0, // default mock value
stockToRemit: 25,
remainingQuantityInStock: 15,
});
});
it('should calculate target stock without remainingQuantityInStock when selected quantity differs from stock to remit', () => {
const {
calculateTargetStock,
getStockToRemit,
} = require('@isa/remission/data-access');
calculateTargetStock.mockReturnValue(80);
getStockToRemit.mockReturnValue(25);
mockRemissionStore.selectedQuantity.set({ 1: 20 }); // Different from stockToRemit
const mockItem = createMockReturnItem({
id: 1,
remainingQuantityInStock: 15,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.targetStock()).toBe(80);
expect(calculateTargetStock).toHaveBeenCalledWith({
availableStock: 100, // default mock value
stockToRemit: 20, // selected quantity, not calculated stockToRemit
});
});
it('should calculate target stock with remainingQuantityInStock when no selected quantity exists', () => {
const {
calculateTargetStock,
getStockToRemit,
} = require('@isa/remission/data-access');
calculateTargetStock.mockReturnValue(75);
getStockToRemit.mockReturnValue(25);
mockRemissionStore.selectedQuantity.set({}); // No selected quantity
const mockItem = createMockReturnItem({
id: 1,
remainingQuantityInStock: 15,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.targetStock()).toBe(75);
expect(calculateTargetStock).toHaveBeenCalledWith({
availableStock: 100, // default mock value
stockToRemit: 25, // calculated stockToRemit
remainingQuantityInStock: 15,
});
});
@@ -275,6 +332,47 @@ describe('RemissionListItemComponent', () => {
});
});
describe('selectedQuantityDiffersFromStockToRemit', () => {
it('should return true when selected quantity differs from stock to remit', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(10);
mockRemissionStore.selectedQuantity.set({ 1: 15 });
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.selectedQuantityDiffersFromStockToRemit()).toBe(true);
});
it('should return false when selected quantity equals stock to remit', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(15);
mockRemissionStore.selectedQuantity.set({ 1: 15 });
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.selectedQuantityDiffersFromStockToRemit()).toBe(false);
});
it('should return false when no selected quantity exists', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(10);
mockRemissionStore.selectedQuantity.set({});
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.selectedQuantityDiffersFromStockToRemit()).toBe(false);
});
});
describe('hasStockToRemit', () => {
it('should return true when stockToRemit > 0', () => {
const { getStockToRemit } = require('@isa/remission/data-access');

View File

@@ -141,6 +141,16 @@ export class RemissionListItemComponent {
() => this.#store.selectedQuantity()?.[this.item().id!],
);
/**
* Computes whether the selected quantity equals the stock to remit.
* This is used to determine if the remission quantity can be changed.
*/
selectedQuantityDiffersFromStockToRemit = computed(
() =>
this.selectedStockToRemit() !== undefined &&
this.selectedStockToRemit() !== this.stockToRemit(),
);
/**
* Computes the stock to remit based on the remission item and available stock.
* Uses the getStockToRemit helper function.
@@ -155,13 +165,21 @@ export class RemissionListItemComponent {
/**
* Computes the target stock after remission.
* @returns The calculated target stock.
* Uses the calculateTargetStock helper function.
* Takes into account the selected quantity and remaining quantity in stock.
*/
targetStock = computed(() =>
calculateTargetStock({
targetStock = computed(() => {
if (this.selectedQuantityDiffersFromStockToRemit()) {
return calculateTargetStock({
availableStock: this.availableStock(),
stockToRemit: this.selectedStockToRemit(),
});
}
return calculateTargetStock({
availableStock: this.availableStock(),
stockToRemit: this.stockToRemit(),
remainingQuantityInStock: this.remainingQuantityInStock(),
}),
);
});
});
}

View File

@@ -1,5 +1,7 @@
<ui-number-input
[message]="data.message"
[subMessage]="data.subMessage"
[subMessageValue]="data.subMessageValue"
[inputLabel]="data.inputLabel"
[inputValue]="data.inputValue"
[inputDefaultValue]="data.inputDefaultValue"

View File

@@ -14,6 +14,12 @@ export interface NumberInputDialogData {
/** The message text to display in the dialog */
message: string;
/** Optional submessage text to display in the dialog */
subMessage?: string;
/** Optional value to display in the submessage */
subMessageValue?: string;
/** The input field label to display */
inputLabel?: string;

View File

@@ -1,6 +1,26 @@
<p class="isa-text-body-1-regular text-isa-neutral-600" data-what="message">
{{ message() }}
</p>
<div class="flex flex-col">
<p class="isa-text-body-1-regular text-isa-neutral-600" data-what="message">
{{ message() }}
</p>
@if (subMessage()) {
<div class="flex flex-row gap-1">
<p
class="isa-text-body-1-regular text-isa-neutral-600"
data-what="sub-message"
>
{{ subMessage() }}
</p>
@if (subMessageValue()) {
<p
class="isa-text-body-1-bold text-isa-neutral-900"
data-what="sub-message-value"
>
{{ subMessageValue() }}
</p>
}
</div>
}
</div>
<div class="flex flex-col gap-8">
<ui-text-field-container>
<ui-text-field size="small" class="w-full">

View File

@@ -55,6 +55,10 @@ import {
export class NumberInputComponent {
message = input.required<string>();
subMessage = input<string | undefined>(undefined);
subMessageValue = input<string | undefined>(undefined);
inputLabel = input<string | undefined>(undefined);
inputValue = model<number | undefined>(undefined);