mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feature(libs-remission): Improvements and Refactoring of Remission List Component
Ref: #5340
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
effect,
|
||||
Signal,
|
||||
untracked,
|
||||
ResourceStatus,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { injectDialog } from '@isa/ui/dialog';
|
||||
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
|
||||
import { RemissionStore, RemissionItem } from '@isa/remission/data-access';
|
||||
|
||||
/**
|
||||
* Configuration for the empty search result handler.
|
||||
* Provides all necessary signals and callbacks for handling empty search scenarios.
|
||||
*/
|
||||
export interface EmptySearchResultHandlerConfig {
|
||||
/**
|
||||
* Signal returning the status of the remission resource.
|
||||
*/
|
||||
remissionResourceStatus: Signal<ResourceStatus>;
|
||||
|
||||
/**
|
||||
* Signal returning the status of the stock resource.
|
||||
*/
|
||||
stockResourceStatus: Signal<ResourceStatus>;
|
||||
|
||||
/**
|
||||
* Signal returning the current search term.
|
||||
*/
|
||||
searchTerm: Signal<string | undefined>;
|
||||
|
||||
/**
|
||||
* Signal returning the number of search hits.
|
||||
*/
|
||||
hits: Signal<number>;
|
||||
|
||||
/**
|
||||
* Signal indicating whether there is a valid search term.
|
||||
*/
|
||||
hasValidSearchTerm: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* Signal indicating whether the search was triggered by user interaction.
|
||||
*/
|
||||
searchTriggeredByUser: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* Signal indicating whether a remission has been started.
|
||||
*/
|
||||
remissionStarted: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* Signal indicating whether the current list type is "Abteilung".
|
||||
*/
|
||||
isDepartment: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* Signal returning the first item in the list (for auto-preselection).
|
||||
*/
|
||||
firstItem: Signal<RemissionItem | undefined>;
|
||||
|
||||
/**
|
||||
* Callback to preselect a remission item.
|
||||
*/
|
||||
preselectItem: (item: RemissionItem) => void;
|
||||
|
||||
/**
|
||||
* Callback to remit items after dialog selection.
|
||||
* @param options - Options for the remit operation
|
||||
*/
|
||||
remitItems: (options: { addItemFlow: boolean }) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Callback to navigate to the default remission list.
|
||||
*/
|
||||
navigateToDefaultList: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Callback to reload the list and return data.
|
||||
*/
|
||||
reloadData: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an effect that handles scenarios where a search yields no or few results.
|
||||
*
|
||||
* This handler implements two behaviors:
|
||||
* 1. **Auto-Preselection**: When exactly one item is found and remission is started,
|
||||
* automatically preselects that item for convenience.
|
||||
* 2. **Empty Search Dialog**: When no items are found after a user-initiated search,
|
||||
* opens a dialog allowing the user to add items to remit.
|
||||
*
|
||||
* @param config - Configuration object containing all required signals and callbacks
|
||||
* @returns The created effect (for potential cleanup if needed)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In a component
|
||||
* emptySearchEffect = injectEmptySearchResultHandler({
|
||||
* remissionResourceStatus: () => this.remissionResource.status(),
|
||||
* stockResourceStatus: () => this.inStockResource.status(),
|
||||
* searchTerm: this.searchTerm,
|
||||
* hits: this.hits,
|
||||
* // ... other config
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* - The effect tracks `remissionResourceStatus`, `stockResourceStatus`, and `searchTerm`
|
||||
* - Other signals are accessed via `untracked()` to avoid unnecessary re-evaluations
|
||||
* - The dialog subscription handles async flows for adding items to remission
|
||||
*/
|
||||
export const injectEmptySearchResultHandler = (
|
||||
config: EmptySearchResultHandlerConfig,
|
||||
) => {
|
||||
const store = inject(RemissionStore);
|
||||
const searchItemToRemitDialog = injectDialog(
|
||||
SearchItemToRemitDialogComponent,
|
||||
);
|
||||
|
||||
return effect(() => {
|
||||
const status = config.remissionResourceStatus();
|
||||
const stockStatus = config.stockResourceStatus();
|
||||
const searchTerm = config.searchTerm();
|
||||
|
||||
// Wait until both resources are resolved
|
||||
if (status !== 'resolved' || stockStatus !== 'resolved') {
|
||||
return;
|
||||
}
|
||||
|
||||
untracked(() => {
|
||||
const hits = config.hits();
|
||||
|
||||
// Early return conditions - only proceed if:
|
||||
// - No hits (hits === 0, so !!hits is false)
|
||||
// - Valid search term exists
|
||||
// - Search was triggered by user
|
||||
if (
|
||||
!!hits ||
|
||||
!searchTerm ||
|
||||
!config.hasValidSearchTerm() ||
|
||||
!config.searchTriggeredByUser()
|
||||
) {
|
||||
// #5338 - Auto-select item if exactly one hit after search
|
||||
if (hits === 1 && config.remissionStarted()) {
|
||||
store.clearSelectedItems();
|
||||
const firstItem = config.firstItem();
|
||||
if (firstItem) {
|
||||
config.preselectItem(firstItem);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Open dialog to allow user to add items when search returns no results
|
||||
searchItemToRemitDialog({
|
||||
data: {
|
||||
searchTerm,
|
||||
},
|
||||
}).closed.subscribe(async (result) => {
|
||||
store.clearSelectedItems();
|
||||
|
||||
if (result) {
|
||||
if (config.remissionStarted()) {
|
||||
// Select all items from dialog result
|
||||
for (const item of result) {
|
||||
if (item?.id) {
|
||||
store.selectRemissionItem(item.id, item);
|
||||
}
|
||||
}
|
||||
// Remit the selected items
|
||||
await config.remitItems({ addItemFlow: true });
|
||||
} else if (config.isDepartment()) {
|
||||
// Navigate to default list if in department mode without active remission
|
||||
await config.navigateToDefaultList();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Always reload data after dialog closes
|
||||
config.reloadData();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export { RemissionActionComponent } from './remission-action.component';
|
||||
export {
|
||||
RemissionActionService,
|
||||
RemitItemsContext,
|
||||
RemitItemsOptions,
|
||||
} from './remission-action.service';
|
||||
@@ -0,0 +1,21 @@
|
||||
@if (remissionStarted()) {
|
||||
<ui-stateful-button
|
||||
(clicked)="remitItems()"
|
||||
(action)="remitItems()"
|
||||
[(state)]="actionService.state"
|
||||
defaultContent="Remittieren"
|
||||
defaultWidth="13rem"
|
||||
[errorContent]="actionService.error()"
|
||||
errorWidth="32rem"
|
||||
errorAction="Erneut versuchen"
|
||||
successContent="Hinzugefügt"
|
||||
successWidth="20rem"
|
||||
size="large"
|
||||
color="brand"
|
||||
[pending]="actionService.inProgress()"
|
||||
[disabled]="isDisabled()"
|
||||
data-what="button"
|
||||
data-which="remit-items"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { StatefulButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
RemissionStore,
|
||||
RemissionItem,
|
||||
RemissionListType,
|
||||
} from '@isa/remission/data-access';
|
||||
import {
|
||||
RemissionActionService,
|
||||
RemitItemsContext,
|
||||
RemitItemsOptions,
|
||||
} from './remission-action.service';
|
||||
|
||||
/**
|
||||
* RemissionActionComponent
|
||||
*
|
||||
* Standalone component that encapsulates the "Remittieren" (remit) button
|
||||
* and its associated logic. Manages the remit workflow including:
|
||||
* - Displaying the stateful button with appropriate states
|
||||
* - Triggering the remit action
|
||||
* - Handling loading, success, and error states
|
||||
*
|
||||
* @remarks
|
||||
* This component requires the RemissionActionService to be provided,
|
||||
* either by itself or by a parent component.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <remi-feature-remission-action
|
||||
* [getAvailableStockForItem]="getAvailableStockForItem"
|
||||
* [selectedRemissionListType]="selectedRemissionListType()"
|
||||
* [disabled]="removeItemInProgress()"
|
||||
* (actionCompleted)="onActionCompleted()"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-action',
|
||||
templateUrl: './remission-action.component.html',
|
||||
styleUrl: './remission-action.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [StatefulButtonComponent],
|
||||
providers: [RemissionActionService],
|
||||
})
|
||||
export class RemissionActionComponent {
|
||||
readonly #store = inject(RemissionStore);
|
||||
readonly actionService = inject(RemissionActionService);
|
||||
|
||||
/**
|
||||
* Function to get available stock for a remission item.
|
||||
* Required for calculating quantities during remit.
|
||||
*/
|
||||
getAvailableStockForItem = input.required<(item: RemissionItem) => number>();
|
||||
|
||||
/**
|
||||
* The currently selected remission list type.
|
||||
* Required for determining item types during remit.
|
||||
*/
|
||||
selectedRemissionListType = input.required<RemissionListType>();
|
||||
|
||||
/**
|
||||
* Additional disabled state from parent component.
|
||||
* Combined with internal disabled logic.
|
||||
*/
|
||||
disabled = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Emitted when the remit action is completed (success or error).
|
||||
* Parent component should use this to reload list data.
|
||||
*/
|
||||
actionCompleted = output<void>();
|
||||
|
||||
/**
|
||||
* Computed signal indicating whether a remission has been started.
|
||||
* The button is only visible when remission is started.
|
||||
*/
|
||||
remissionStarted = computed(() => this.#store.remissionStarted());
|
||||
|
||||
/**
|
||||
* Computed signal indicating whether there are selected items.
|
||||
*/
|
||||
hasSelectedItems = computed(
|
||||
() => Object.keys(this.#store.selectedItems()).length > 0,
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed signal for the combined disabled state.
|
||||
* Button is disabled when:
|
||||
* - No items are selected
|
||||
* - An external disabled condition is true
|
||||
* - A remit operation is in progress
|
||||
*/
|
||||
isDisabled = computed(
|
||||
() =>
|
||||
!this.hasSelectedItems() ||
|
||||
this.disabled() ||
|
||||
this.actionService.inProgress(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles the remit button click.
|
||||
* Delegates to the action service and emits completion event.
|
||||
*
|
||||
* @param options - Options for the remit operation
|
||||
*/
|
||||
async remitItems(
|
||||
options: RemitItemsOptions = { addItemFlow: false },
|
||||
): Promise<void> {
|
||||
const context: RemitItemsContext = {
|
||||
getAvailableStockForItem: this.getAvailableStockForItem(),
|
||||
selectedRemissionListType: this.selectedRemissionListType(),
|
||||
};
|
||||
|
||||
await this.actionService.remitItems(context, options);
|
||||
this.actionCompleted.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import {
|
||||
RemissionStore,
|
||||
RemissionReturnReceiptService,
|
||||
RemissionListType,
|
||||
RemissionResponseArgsErrorMessage,
|
||||
RemissionItem,
|
||||
getStockToRemit,
|
||||
getItemType,
|
||||
} from '@isa/remission/data-access';
|
||||
import { StatefulButtonState } from '@isa/ui/buttons';
|
||||
import { injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Configuration options for the remit items operation.
|
||||
*/
|
||||
export interface RemitItemsOptions {
|
||||
/** Whether this operation is part of an add-item flow (e.g., from search dialog) */
|
||||
addItemFlow: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context required for remitting items.
|
||||
* Provides stock information lookup for calculating quantities.
|
||||
*/
|
||||
export interface RemitItemsContext {
|
||||
/** Function to get available stock for a remission item */
|
||||
getAvailableStockForItem: (item: RemissionItem) => number;
|
||||
/** The currently selected remission list type */
|
||||
selectedRemissionListType: RemissionListType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service responsible for handling the remission action workflow.
|
||||
* Manages the state and logic for remitting selected items.
|
||||
*
|
||||
* This service encapsulates:
|
||||
* - State management for the remit button (progress, error, success states)
|
||||
* - The remitItems business logic
|
||||
* - Error handling and user feedback
|
||||
* - Navigation after successful remission
|
||||
*
|
||||
* @remarks
|
||||
* This service should be provided at the component level, not root level,
|
||||
* as it maintains UI state specific to a single remission action context.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In component providers
|
||||
* providers: [RemissionActionService]
|
||||
*
|
||||
* // Usage
|
||||
* readonly actionService = inject(RemissionActionService);
|
||||
* await this.actionService.remitItems(context);
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class RemissionActionService {
|
||||
readonly #store = inject(RemissionStore);
|
||||
readonly #remissionReturnReceiptService = inject(
|
||||
RemissionReturnReceiptService,
|
||||
);
|
||||
readonly #errorDialog = injectFeedbackErrorDialog();
|
||||
|
||||
readonly #logger = logger(() => ({
|
||||
service: 'RemissionActionService',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Signal representing the current state of the remit button.
|
||||
*/
|
||||
readonly state = signal<StatefulButtonState>('default');
|
||||
|
||||
/**
|
||||
* Signal containing the current error message, if any.
|
||||
*/
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
/**
|
||||
* Signal indicating whether a remit operation is currently in progress.
|
||||
*/
|
||||
readonly inProgress = signal(false);
|
||||
|
||||
/**
|
||||
* Computed signal indicating whether there are selected items in the store.
|
||||
*/
|
||||
get hasSelectedItems(): boolean {
|
||||
return Object.keys(this.#store.selectedItems()).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed signal indicating whether a remission has been started.
|
||||
*/
|
||||
get remissionStarted(): boolean {
|
||||
return this.#store.remissionStarted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the process to remit selected items.
|
||||
*
|
||||
* If remission is already started, items are added directly to the remission.
|
||||
* Handles the full workflow including:
|
||||
* - Preventing duplicate operations
|
||||
* - Processing each selected item
|
||||
* - Error handling with user feedback
|
||||
* - State management for UI feedback
|
||||
*
|
||||
* @param context - Context providing stock information and list type
|
||||
* @param options - Options for the remit operation
|
||||
* @returns A promise that resolves when the operation is complete
|
||||
*/
|
||||
async remitItems(
|
||||
context: RemitItemsContext,
|
||||
options: RemitItemsOptions = { addItemFlow: false },
|
||||
): Promise<void> {
|
||||
if (this.inProgress()) {
|
||||
return;
|
||||
}
|
||||
this.inProgress.set(true);
|
||||
|
||||
try {
|
||||
await this.#processSelectedItems(context, options);
|
||||
this.state.set('success');
|
||||
} catch (error) {
|
||||
await this.#handleRemitItemsError(error);
|
||||
}
|
||||
|
||||
this.#store.clearSelectedItems();
|
||||
this.inProgress.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all selected items for remission.
|
||||
* @param context - Context providing stock information and list type
|
||||
* @param options - Options for the remit operation
|
||||
*/
|
||||
async #processSelectedItems(
|
||||
context: RemitItemsContext,
|
||||
options: RemitItemsOptions,
|
||||
): Promise<void> {
|
||||
// #5273, #5280 Fix - Bei gestarteter Remission dürfen Items die über den AddItemDialog
|
||||
// hinzugefügt und direkt remittiert werden, nur als ReturnItem (statt ReturnSuggestion)
|
||||
// zum WBS hinzugefügt werden
|
||||
const remissionListType = options.addItemFlow
|
||||
? RemissionListType.Pflicht
|
||||
: context.selectedRemissionListType;
|
||||
|
||||
const selected = this.#store.selectedItems();
|
||||
const quantities = this.#store.selectedQuantity();
|
||||
|
||||
for (const [remissionItemId, item] of Object.entries(selected)) {
|
||||
const returnId = this.#store.returnId();
|
||||
const receiptId = this.#store.receiptId();
|
||||
const remissionItemIdNumber = Number(remissionItemId);
|
||||
const quantity = quantities[remissionItemIdNumber];
|
||||
const inStock = context.getAvailableStockForItem(item);
|
||||
const stockToRemit = getStockToRemit({
|
||||
remissionItem: item,
|
||||
remissionListType,
|
||||
availableStock: inStock,
|
||||
});
|
||||
const quantityToRemit = quantity ?? stockToRemit;
|
||||
|
||||
if (returnId && receiptId) {
|
||||
await this.#remissionReturnReceiptService.remitItem({
|
||||
itemId: remissionItemIdNumber,
|
||||
addItem: {
|
||||
returnId,
|
||||
receiptId,
|
||||
quantity: quantityToRemit,
|
||||
inStock,
|
||||
impedimentComment: stockToRemit > quantity ? 'Restmenge' : '',
|
||||
remainingQuantity:
|
||||
isNaN(quantity) || inStock - quantity <= 0
|
||||
? undefined
|
||||
: inStock - quantity,
|
||||
},
|
||||
type: getItemType(item, remissionListType),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the remission of items.
|
||||
* Logs the error, displays an error dialog, and updates state.
|
||||
*
|
||||
* @param error - The error object caught during the remission process
|
||||
*/
|
||||
async #handleRemitItemsError(error: unknown): Promise<void> {
|
||||
this.#logger.error('Failed to remit items', error as Error);
|
||||
|
||||
const errorMessage =
|
||||
(error as { error?: { message?: string }; message?: string })?.error
|
||||
?.message ??
|
||||
(error as { message?: string })?.message ??
|
||||
'Artikel konnten nicht remittiert werden';
|
||||
|
||||
this.error.set(errorMessage);
|
||||
|
||||
await firstValueFrom(
|
||||
this.#errorDialog({
|
||||
data: {
|
||||
errorMessage,
|
||||
},
|
||||
}).closed,
|
||||
);
|
||||
|
||||
if (errorMessage === RemissionResponseArgsErrorMessage.AlreadyCompleted) {
|
||||
this.#store.clearState();
|
||||
}
|
||||
|
||||
this.state.set('error');
|
||||
}
|
||||
}
|
||||
@@ -59,23 +59,10 @@
|
||||
[class.scroll-top-button-spacing-bottom]="remissionStarted()"
|
||||
></utils-scroll-top-button>
|
||||
|
||||
@if (remissionStarted()) {
|
||||
<ui-stateful-button
|
||||
class="flex flex-col self-end fixed bottom-6 mr-6"
|
||||
(clicked)="remitItems()"
|
||||
(action)="remitItems()"
|
||||
[(state)]="remitItemsState"
|
||||
defaultContent="Remittieren"
|
||||
defaultWidth="13rem"
|
||||
[errorContent]="remitItemsError()"
|
||||
errorWidth="32rem"
|
||||
errorAction="Erneut versuchen"
|
||||
successContent="Hinzugefügt"
|
||||
successWidth="20rem"
|
||||
size="large"
|
||||
color="brand"
|
||||
[pending]="remitItemsInProgress()"
|
||||
[disabled]="!hasSelectedItems() || removeItemInProgress()"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
<remi-feature-remission-action
|
||||
class="flex flex-col self-end fixed bottom-6 mr-6"
|
||||
[getAvailableStockForItem]="getAvailableStockForItem.bind(this)"
|
||||
[selectedRemissionListType]="selectedRemissionListType()"
|
||||
[disabled]="removeItemInProgress()"
|
||||
(actionCompleted)="reloadListAndReturnData()"
|
||||
></remi-feature-remission-action>
|
||||
|
||||
@@ -3,10 +3,9 @@ import {
|
||||
Component,
|
||||
inject,
|
||||
computed,
|
||||
effect,
|
||||
untracked,
|
||||
signal,
|
||||
linkedSignal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
@@ -29,12 +28,9 @@ import {
|
||||
createRemissionProductGroupResource,
|
||||
} from './resources';
|
||||
import { injectRemissionListType } from './injects/inject-remission-list-type';
|
||||
import { injectEmptySearchResultHandler } from './injects/inject-empty-search-result-handler';
|
||||
import { RemissionListItemComponent } from './remission-list-item/remission-list-item.component';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
StatefulButtonComponent,
|
||||
StatefulButtonState,
|
||||
} from '@isa/ui/buttons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
ReturnItem,
|
||||
StockInfo,
|
||||
@@ -42,22 +38,16 @@ import {
|
||||
RemissionStore,
|
||||
RemissionItem,
|
||||
calculateAvailableStock,
|
||||
RemissionReturnReceiptService,
|
||||
getStockToRemit,
|
||||
RemissionListType,
|
||||
RemissionResponseArgsErrorMessage,
|
||||
UpdateItem,
|
||||
orderByListItems,
|
||||
getItemType,
|
||||
getStockToRemit,
|
||||
} from '@isa/remission/data-access';
|
||||
import { injectDialog, injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
|
||||
import { RemissionReturnCardComponent } from './remission-return-card/remission-return-card.component';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { RemissionListDepartmentElementsComponent } from './remission-list-department-elements/remission-list-department-elements.component';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { RemissionListEmptyStateComponent } from './remission-list-empty-state/remission-list-empty-state.component';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { RemissionActionComponent } from './remission-action';
|
||||
|
||||
function querySettingsFactory() {
|
||||
return inject(ActivatedRoute).snapshot.data['querySettings'];
|
||||
@@ -96,7 +86,7 @@ function querySettingsFactory() {
|
||||
RemissionListSelectComponent,
|
||||
RemissionListItemComponent,
|
||||
IconButtonComponent,
|
||||
StatefulButtonComponent,
|
||||
RemissionActionComponent,
|
||||
RemissionListDepartmentElementsComponent,
|
||||
RemissionListEmptyStateComponent,
|
||||
ScrollTopButtonComponent,
|
||||
@@ -125,8 +115,10 @@ export class RemissionListComponent {
|
||||
*/
|
||||
activatedTabId = injectTabId();
|
||||
|
||||
searchItemToRemitDialog = injectDialog(SearchItemToRemitDialogComponent);
|
||||
errorDialog = injectFeedbackErrorDialog();
|
||||
/**
|
||||
* Reference to the RemissionActionComponent for triggering remit actions.
|
||||
*/
|
||||
remissionAction = viewChild(RemissionActionComponent);
|
||||
|
||||
/**
|
||||
* FilterService instance for managing filter state and queries.
|
||||
@@ -140,20 +132,6 @@ export class RemissionListComponent {
|
||||
*/
|
||||
#store = inject(RemissionStore);
|
||||
|
||||
/**
|
||||
* RemissionReturnReceiptService instance for handling return receipt operations.
|
||||
* @private
|
||||
*/
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
/**
|
||||
* Logger instance for logging component events and errors.
|
||||
* @private
|
||||
*/
|
||||
#logger = logger(() => ({
|
||||
component: 'RemissionListComponent',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Restores scroll position when navigating back to this component.
|
||||
*/
|
||||
@@ -294,32 +272,6 @@ export class RemissionListComponent {
|
||||
*/
|
||||
remissionStarted = computed(() => this.#store.remissionStarted());
|
||||
|
||||
/**
|
||||
* Computed signal indicating whether there are selected items in the remission store.
|
||||
* @returns True if there are selected items, false otherwise.
|
||||
*/
|
||||
hasSelectedItems = computed(() => {
|
||||
return Object.keys(this.#store.selectedItems()).length > 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal for the current remission list type.
|
||||
* @returns The current RemissionListType.
|
||||
*/
|
||||
remitItemsState = signal<StatefulButtonState>('default');
|
||||
|
||||
/**
|
||||
* Signal for any error messages related to remission items.
|
||||
* @returns Error message string or null if no error.
|
||||
*/
|
||||
remitItemsError = signal<string | null>(null);
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission items are currently being processed.
|
||||
* @returns True if in progress, false otherwise.
|
||||
*/
|
||||
remitItemsInProgress = signal(false);
|
||||
|
||||
/**
|
||||
* Commits the current filter state and triggers a new search.
|
||||
*
|
||||
@@ -414,132 +366,35 @@ export class RemissionListComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Effect that handles scenarios where a search yields no results.
|
||||
* If the search was user-initiated and returned no hits, it opens a dialog
|
||||
* to allow the user to add a new item to remit.
|
||||
* If only one hit is found and a remission is started, it selects that item automatically.
|
||||
* This effect runs whenever the remission or stock resource status changes,
|
||||
* or when the search term changes.
|
||||
* It ensures that the user is prompted appropriately based on their actions and the current state of the remission process.
|
||||
* It also checks if the remission is started or if the list type is 'Abteilung' to determine navigation behavior.
|
||||
* @see {@link
|
||||
* https://angular.dev/guide/effects} for more information on Angular effects.
|
||||
* @remarks This effect uses `untracked` to avoid unnecessary re-evaluations
|
||||
* when accessing certain signals.
|
||||
* Computed signal returning the first item in the list.
|
||||
* Used for auto-preselection when exactly one item is found.
|
||||
*/
|
||||
emptySearchResultEffect = effect(() => {
|
||||
const status = this.remissionResource.status();
|
||||
const stockStatus = this.inStockResource.status();
|
||||
const searchTerm: string | undefined = this.searchTerm();
|
||||
#firstItem = computed(() => this.items()[0]);
|
||||
|
||||
if (status !== 'resolved' || stockStatus !== 'resolved') {
|
||||
return;
|
||||
}
|
||||
|
||||
untracked(() => {
|
||||
const hits = this.hits();
|
||||
|
||||
// #5338 - Select item automatically if only one hit after search
|
||||
if (
|
||||
!!hits ||
|
||||
!searchTerm ||
|
||||
!this.hasValidSearchTerm() ||
|
||||
!this.searchTriggeredByUser()
|
||||
) {
|
||||
if (hits === 1 && this.remissionStarted()) {
|
||||
this.#store.clearSelectedItems();
|
||||
this.preselectRemissionItem(this.items()[0]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchItemToRemitDialog({
|
||||
data: {
|
||||
searchTerm,
|
||||
},
|
||||
}).closed.subscribe(async (result) => {
|
||||
this.#store.clearSelectedItems();
|
||||
if (result) {
|
||||
if (this.remissionStarted()) {
|
||||
for (const item of result) {
|
||||
if (item?.id) {
|
||||
this.#store.selectRemissionItem(item.id, item);
|
||||
}
|
||||
}
|
||||
await this.remitItems({ addItemFlow: true });
|
||||
} else if (this.isDepartment()) {
|
||||
return await this.navigateToDefaultRemissionList();
|
||||
}
|
||||
}
|
||||
this.reloadListAndReturnData();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Improvement - In Separate Komponente zusammen mit Remi-Button Auslagern
|
||||
/**
|
||||
* Initiates the process to remit selected items.
|
||||
* If remission is already started, items are added directly to the remission.
|
||||
* If not, navigates to the default remission list.
|
||||
* @param options - Options for remitting items, including whether it's part of an add-item flow.
|
||||
* @returns A promise that resolves when the operation is complete.
|
||||
* Effect that handles scenarios where a search yields no or few results.
|
||||
* - Auto-preselects item when exactly one hit is found
|
||||
* - Opens dialog to add items when no results are found
|
||||
*
|
||||
* @see injectEmptySearchResultHandler for implementation details
|
||||
*/
|
||||
async remitItems(options: { addItemFlow: boolean } = { addItemFlow: false }) {
|
||||
if (this.remitItemsInProgress()) {
|
||||
return;
|
||||
}
|
||||
this.remitItemsInProgress.set(true);
|
||||
|
||||
try {
|
||||
// #5273, #5280 Fix - Bei gestarteter Remission dürfen Items die über den AddItemDialog hinzugefügt und direkt remittiert werden, nur als ReturnItem (statt ReturnSuggestion) zum WBS hinzugefügt werden
|
||||
const remissionListType = options.addItemFlow
|
||||
? RemissionListType.Pflicht
|
||||
: this.selectedRemissionListType();
|
||||
|
||||
const selected = this.#store.selectedItems();
|
||||
const quantities = this.#store.selectedQuantity();
|
||||
|
||||
for (const [remissionItemId, item] of Object.entries(selected)) {
|
||||
const returnId = this.#store.returnId();
|
||||
const receiptId = this.#store.receiptId();
|
||||
const remissionItemIdNumber = Number(remissionItemId);
|
||||
const quantity = quantities[remissionItemIdNumber];
|
||||
const inStock = this.getAvailableStockForItem(item);
|
||||
const stockToRemit = getStockToRemit({
|
||||
remissionItem: item,
|
||||
remissionListType,
|
||||
availableStock: inStock,
|
||||
});
|
||||
const quantityToRemit = quantity ?? stockToRemit;
|
||||
|
||||
if (returnId && receiptId) {
|
||||
await this.#remissionReturnReceiptService.remitItem({
|
||||
itemId: remissionItemIdNumber,
|
||||
addItem: {
|
||||
returnId,
|
||||
receiptId,
|
||||
quantity: quantityToRemit,
|
||||
inStock,
|
||||
impedimentComment: stockToRemit > quantity ? 'Restmenge' : '',
|
||||
remainingQuantity:
|
||||
isNaN(quantity) || inStock - quantity <= 0
|
||||
? undefined
|
||||
: inStock - quantity,
|
||||
},
|
||||
type: getItemType(item, remissionListType),
|
||||
});
|
||||
}
|
||||
}
|
||||
this.remitItemsState.set('success');
|
||||
this.reloadListAndReturnData();
|
||||
} catch (error) {
|
||||
await this.handleRemitItemsError(error);
|
||||
}
|
||||
|
||||
this.#store.clearSelectedItems();
|
||||
this.remitItemsInProgress.set(false);
|
||||
}
|
||||
emptySearchResultEffect = injectEmptySearchResultHandler({
|
||||
remissionResourceStatus: computed(() => this.remissionResource.status()),
|
||||
stockResourceStatus: computed(() => this.inStockResource.status()),
|
||||
searchTerm: this.searchTerm,
|
||||
hits: this.hits,
|
||||
hasValidSearchTerm: this.hasValidSearchTerm,
|
||||
searchTriggeredByUser: this.searchTriggeredByUser,
|
||||
remissionStarted: this.remissionStarted,
|
||||
isDepartment: this.isDepartment,
|
||||
firstItem: this.#firstItem,
|
||||
preselectItem: (item) => this.preselectRemissionItem(item),
|
||||
remitItems: async (options) => {
|
||||
await this.remissionAction()?.remitItems(options);
|
||||
},
|
||||
navigateToDefaultList: () => this.navigateToDefaultRemissionList(),
|
||||
reloadData: () => this.reloadListAndReturnData(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Reloads the remission list and return data.
|
||||
@@ -572,41 +427,6 @@ export class RemissionListComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the remission of items.
|
||||
* Logs the error, displays an error dialog, and reloads the list and return data.
|
||||
* If the error indicates that the remission is already completed, it clears the remission state.
|
||||
* Sets the stateful button to 'error' to indicate the failure.
|
||||
* @param error - The error object caught during the remission process.
|
||||
* @returns A promise that resolves when the error handling is complete.
|
||||
*/
|
||||
async handleRemitItemsError(error: any) {
|
||||
this.#logger.error('Failed to remit items', error);
|
||||
|
||||
const errorMessage =
|
||||
error?.error?.message ??
|
||||
error?.message ??
|
||||
'Artikel konnten nicht remittiert werden';
|
||||
|
||||
this.remitItemsError.set(errorMessage);
|
||||
|
||||
await firstValueFrom(
|
||||
this.errorDialog({
|
||||
data: {
|
||||
errorMessage,
|
||||
},
|
||||
}).closed,
|
||||
);
|
||||
|
||||
if (errorMessage === RemissionResponseArgsErrorMessage.AlreadyCompleted) {
|
||||
this.#store.clearState();
|
||||
}
|
||||
|
||||
this.reloadListAndReturnData();
|
||||
|
||||
this.remitItemsState.set('error'); // Stateful-Button auf Error setzen
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the default 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.
|
||||
|
||||
Reference in New Issue
Block a user