mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
- feat(oms-data-access, oms-return-details): add processed quantity helper and refactor item controls - feat(ui-input-controls, oms-return-details): add disabled styling and logic for dropdowns - feat(oms-return-details): improve dropdown accessibility and disabled state handling Refs: #5144 #5141 #5099
This commit is contained in:
committed by
Lorenz Hilpert
parent
80fb65ffc4
commit
b589dc21cd
@@ -0,0 +1,62 @@
|
||||
import { getReceiptItemReturnedQuantity } from './get-receipt-item-returned-quantity.helper';
|
||||
|
||||
describe('getReceiptItemReturnedQuantity', () => {
|
||||
it('should return 0 when referencedInOtherReceipts is missing', () => {
|
||||
// Arrange
|
||||
const item = { id: 123 };
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemReturnedQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 when referencedInOtherReceipts is an empty array', () => {
|
||||
// Arrange
|
||||
const item = { id: 123, referencedInOtherReceipts: [] };
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemReturnedQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should sum all quantities in referencedInOtherReceipts', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 123,
|
||||
referencedInOtherReceipts: [
|
||||
{ quantity: 2 },
|
||||
{ quantity: 3 },
|
||||
{ quantity: 5 },
|
||||
],
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemReturnedQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
|
||||
it('should treat null or undefined quantities as 0', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 123,
|
||||
referencedInOtherReceipts: [
|
||||
{ quantity: 2 },
|
||||
{ quantity: null as unknown as number },
|
||||
{ quantity: undefined as unknown as number },
|
||||
{ quantity: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemReturnedQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Calculates the total quantity of a receipt item that has already been returned.
|
||||
*
|
||||
* This function sums the `quantity` values from the `referencedInOtherReceipts` array,
|
||||
* representing how many units of the item have been processed for return in other receipts.
|
||||
* If no return history is present, it returns 0.
|
||||
*
|
||||
* @param item - The receipt item object containing an `id` and optional return history.
|
||||
* @returns The total number of units already returned for this receipt item.
|
||||
*/
|
||||
export function getReceiptItemReturnedQuantity(item: {
|
||||
id: number;
|
||||
referencedInOtherReceipts?: { quantity: number }[];
|
||||
}): number {
|
||||
return (
|
||||
item.referencedInOtherReceipts?.reduce(
|
||||
(sum, receipt) => sum + (receipt?.quantity || 0),
|
||||
0,
|
||||
) || 0
|
||||
); // Default to 0 if not specified
|
||||
}
|
||||
@@ -13,3 +13,4 @@ export * from './get-tolino-questions.helper';
|
||||
export * from './receipt-item-has-category.helper';
|
||||
export * from './return-details-mapping.helper';
|
||||
export * from './return-receipt-values-mapping.helper';
|
||||
export * from './get-receipt-item-returned-quantity.helper';
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@if (canReturnReceiptItem()) {
|
||||
<div>
|
||||
@if (quantityDropdownValues().length > 1) {
|
||||
<ui-dropdown [value]="quantity()" (valueChange)="setQuantity($event)">
|
||||
<ui-dropdown
|
||||
class="quantity-dropdown"
|
||||
[disabled]="!canReturnReceiptItem()"
|
||||
[value]="availableQuantity()"
|
||||
(valueChange)="setQuantity($event)"
|
||||
>
|
||||
@for (quantity of quantityDropdownValues(); track quantity) {
|
||||
<ui-dropdown-option [value]="quantity">{{
|
||||
quantity
|
||||
@@ -11,6 +16,8 @@
|
||||
|
||||
<ui-dropdown
|
||||
label="Produktart"
|
||||
class="product-dropdown"
|
||||
[disabled]="!canReturnReceiptItem()"
|
||||
[value]="productCategory()"
|
||||
(valueChange)="setProductCategory($event)"
|
||||
>
|
||||
@@ -18,7 +25,9 @@
|
||||
<ui-dropdown-option [value]="kv.key">{{ kv.value }}</ui-dropdown-option>
|
||||
}
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
|
||||
@if (canReturnReceiptItem()) {
|
||||
@if (!canReturnResource.isLoading() && selectable()) {
|
||||
<ui-checkbox appearance="bullet">
|
||||
<input
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
:host {
|
||||
@apply flex justify-center items-center gap-4;
|
||||
|
||||
:has(.product-dropdown):has(.quantity-dropdown) {
|
||||
.quantity-dropdown.ui-dropdown {
|
||||
@apply border-r-0 pr-4;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
&.ui-dropdown__accent-outline {
|
||||
&.open {
|
||||
@apply border-isa-accent-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-dropdown.ui-dropdown {
|
||||
@apply border-l-0 pl-4;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
&.ui-dropdown__accent-outline {
|
||||
&.open {
|
||||
@apply border-isa-accent-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { provideLoggerContext } from '@isa/core/logging';
|
||||
import {
|
||||
canReturnReceiptItem,
|
||||
getReceiptItemReturnedQuantity,
|
||||
getReceiptItemProductCategory,
|
||||
getReceiptItemQuantity,
|
||||
ProductCategory,
|
||||
@@ -65,13 +66,46 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
|
||||
|
||||
availableCategories = this.#returnDetailsService.availableCategories();
|
||||
|
||||
/**
|
||||
* Computes the quantity of the current receipt item that has already been returned.
|
||||
*
|
||||
* This value is derived from the item's return history and is used to indicate
|
||||
* how many units have already been processed for return.
|
||||
*
|
||||
* @returns The number of units already returned for this receipt item.
|
||||
*/
|
||||
returnedQuantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemReturnedQuantity(item);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the total quantity for the current receipt item.
|
||||
* Represents the original quantity as recorded in the receipt.
|
||||
*
|
||||
* @returns The total quantity for the item.
|
||||
*/
|
||||
quantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemQuantity(item);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the quantity of the item that is still available for return.
|
||||
* Calculated as the difference between the total quantity and the returned quantity.
|
||||
*
|
||||
* @returns The number of units available to be returned.
|
||||
*/
|
||||
availableQuantity = computed(() => this.quantity() - this.returnedQuantity());
|
||||
|
||||
/**
|
||||
* Generates the list of selectable quantities for the dropdown.
|
||||
* The values range from 1 up to the available quantity.
|
||||
*
|
||||
* @returns An array of selectable quantity values.
|
||||
*/
|
||||
quantityDropdownValues = computed(() => {
|
||||
const itemQuantity = this.item().quantity.quantity;
|
||||
const itemQuantity = this.availableQuantity();
|
||||
return Array.from({ length: itemQuantity }, (_, i) => i + 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -73,3 +73,12 @@
|
||||
<span>{{ canReturnMessage() }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (returnedQuantity() > 0 && itemQuantity() !== returnedQuantity()) {
|
||||
<div
|
||||
class="flex items-center self-start text-isa-neutral-600 isa-text-body-2-bold pb-6"
|
||||
>
|
||||
Es wurden bereits {{ returnedQuantity() }} von {{ itemQuantity() }} Artikel
|
||||
zurückgegeben.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { isaActionClose, ProductFormatIconGroup } from '@isa/icons';
|
||||
import {
|
||||
getReceiptItemAction,
|
||||
getReceiptItemReturnedQuantity,
|
||||
getReceiptItemQuantity,
|
||||
ReceiptItem,
|
||||
ReturnDetailsStore,
|
||||
} from '@isa/oms/data-access';
|
||||
@@ -88,4 +90,28 @@ export class ReturnDetailsOrderGroupItemComponent {
|
||||
|
||||
return canReturnMessage ?? '';
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the quantity of the current receipt item that has already been returned.
|
||||
*
|
||||
* This value is derived using the item's return history and is used to display
|
||||
* how many units of this item have been processed for return so far.
|
||||
*
|
||||
* @returns The number of units already returned for this receipt item.
|
||||
*/
|
||||
returnedQuantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemReturnedQuantity(item);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the total quantity for the current receipt item.
|
||||
* Represents the original quantity of the item as recorded in the receipt.
|
||||
*
|
||||
* @returns The total quantity for the item.
|
||||
*/
|
||||
itemQuantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemQuantity(item);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,18 @@
|
||||
ng-icon {
|
||||
@apply size-6;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@apply text-isa-white bg-isa-neutral-400 border-isa-neutral-400 cursor-default;
|
||||
|
||||
&:hover {
|
||||
@apply bg-isa-neutral-400 border-isa-neutral-400;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply border-isa-neutral-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dropdown__accent-outline {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { DropdownAppearance } from './dropdown.types';
|
||||
template: '<ng-content></ng-content>',
|
||||
host: {
|
||||
'[class]': '["ui-dropdown-option", activeClass(), selectedClass()]',
|
||||
'role': 'option',
|
||||
role: 'option',
|
||||
'[attr.aria-selected]': 'selected()',
|
||||
'[attr.tabindex]': '-1',
|
||||
'(click)': 'select()',
|
||||
@@ -75,11 +75,16 @@ export class DropdownOptionComponent<T> implements Highlightable {
|
||||
imports: [NgIconComponent, CdkConnectedOverlay],
|
||||
providers: [
|
||||
provideIcons({ isaActionChevronUp, isaActionChevronDown }),
|
||||
{ provide: NG_VALUE_ACCESSOR, useExisting: DropdownButtonComponent, multi: true },
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: DropdownButtonComponent,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
'[class]': '["ui-dropdown", appearanceClass(), isOpenClass()]',
|
||||
'role': 'combobox',
|
||||
'[class]':
|
||||
'["ui-dropdown", appearanceClass(), isOpenClass(), disabledClass()]',
|
||||
role: 'combobox',
|
||||
'aria-haspopup': 'listbox',
|
||||
'[attr.id]': 'id()',
|
||||
'[attr.tabindex]': 'disabled() ? -1 : tabIndex()',
|
||||
@@ -87,10 +92,13 @@ export class DropdownOptionComponent<T> implements Highlightable {
|
||||
'(keydown)': 'keyManger?.onKeydown($event)',
|
||||
'(keydown.enter)': 'select(keyManger.activeItem); close()',
|
||||
'(keydown.escape)': 'close()',
|
||||
'(click)': 'isOpen() ? close() : open()',
|
||||
'(click)':
|
||||
'disabled() ? $event.stopImmediatePropagation() : (isOpen() ? close() : open())',
|
||||
},
|
||||
})
|
||||
export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterViewInit {
|
||||
export class DropdownButtonComponent<T>
|
||||
implements ControlValueAccessor, AfterViewInit
|
||||
{
|
||||
readonly init = signal(false);
|
||||
private elementRef = inject(ElementRef);
|
||||
|
||||
@@ -102,6 +110,8 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
|
||||
appearanceClass = computed(() => `ui-dropdown__${this.appearance()}`);
|
||||
|
||||
disabledClass = computed(() => (this.disabled() ? 'disabled' : ''));
|
||||
|
||||
id = input<string>();
|
||||
|
||||
value = model<T>();
|
||||
@@ -137,7 +147,9 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
|
||||
isOpenClass = computed(() => (this.isOpen() ? 'open' : ''));
|
||||
|
||||
isOpenIcon = computed(() => (this.isOpen() ? 'isaActionChevronUp' : 'isaActionChevronDown'));
|
||||
isOpenIcon = computed(() =>
|
||||
this.isOpen() ? 'isaActionChevronUp' : 'isaActionChevronDown',
|
||||
);
|
||||
|
||||
viewLabel = computed(() => {
|
||||
if (!this.showSelectedValue()) {
|
||||
@@ -159,9 +171,9 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
return;
|
||||
}
|
||||
this.keyManger?.destroy();
|
||||
this.keyManger = new ActiveDescendantKeyManager<DropdownOptionComponent<T>>(
|
||||
this.options(),
|
||||
).withWrap();
|
||||
this.keyManger = new ActiveDescendantKeyManager<
|
||||
DropdownOptionComponent<T>
|
||||
>(this.options()).withWrap();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,7 +215,10 @@ export class DropdownButtonComponent<T> implements ControlValueAccessor, AfterVi
|
||||
this.disabled.set(isDisabled);
|
||||
}
|
||||
|
||||
select(option: DropdownOptionComponent<T>, options: { emit: boolean } = { emit: true }) {
|
||||
select(
|
||||
option: DropdownOptionComponent<T>,
|
||||
options: { emit: boolean } = { emit: true },
|
||||
) {
|
||||
this.value.set(option.value());
|
||||
|
||||
if (options.emit) {
|
||||
|
||||
Reference in New Issue
Block a user