mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat(oms-data-access, oms-return-details, oms-return-process): improve canReturn logic and UX for return eligibility
- Refactored `ReturnCanReturnService` to use a type guard for input discrimination and improved error handling, removing logger side effects for stricter error propagation. - Updated `ReturnDetailsService` to delegate canReturn checks to `ReturnCanReturnService` for category-based eligibility, ensuring type safety and code reuse. - Enhanced `ReturnDetailsOrderGroupItemComponent`: - Added spinner feedback when canReturn is loading. - Used endpoint result for eligibility and message display, falling back to item actions if necessary. - Improved state management for dropdowns and selection, with robust error logging. - Updated `ReturnDetailsOrderGroupComponent` to only allow selection of items with known categories and eligible for return. - Improved `ReturnProcessItemComponent`: - Added loading spinner for canReturn backend checks. - Used endpoint result for eligibility and messaging. - Added robust error logging and effect-based async state management. - Updated `ReturnProcessComponent` to check canReturn for all processes asynchronously, with error handling and correct signal updates. - Improved templates to show loading indicators and correct eligibility messages based on backend and frontend checks. Ref: #5088
This commit is contained in:
1
libs/oms/data-access/src/lib/guards/index.ts
Normal file
1
libs/oms/data-access/src/lib/guards/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './is-return-process-type.guard';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReturnProcess } from '../models';
|
||||
|
||||
export function isReturnProcessTypeGuard(
|
||||
input: unknown,
|
||||
): input is ReturnProcess {
|
||||
if (typeof input !== 'object' || input === null) return false;
|
||||
|
||||
const inp = input as Partial<ReturnProcess>;
|
||||
|
||||
return (
|
||||
typeof inp.id === 'number' &&
|
||||
typeof inp.processId === 'number' &&
|
||||
typeof inp.receiptId === 'number' &&
|
||||
typeof inp.answers === 'object' &&
|
||||
inp.answers !== null &&
|
||||
'receiptItem' in inp
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
ReceiptService,
|
||||
ReturnReceiptValuesDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { ReturnReceiptValues, ReturnReceiptValuesSchema } from './schemas';
|
||||
import { debounceTime, firstValueFrom, map } from 'rxjs';
|
||||
import { CanReturn, ReturnProcess } from './models';
|
||||
@@ -13,25 +12,94 @@ import {
|
||||
returnReceiptValuesMapping,
|
||||
} from './helpers/return-process';
|
||||
import { memorize } from '@utils/common';
|
||||
import { isReturnProcessTypeGuard } from './guards';
|
||||
|
||||
/**
|
||||
* Service for determining if a return process can proceed based on
|
||||
* provided process data or mapped receipt values.
|
||||
*
|
||||
* - Validates input using Zod schemas.
|
||||
* - Handles both ReturnProcess and ReturnReceiptValues as input.
|
||||
* - Calls backend API to check if return is possible.
|
||||
*
|
||||
* @remarks
|
||||
* - Uses memoization to cache results for identical inputs.
|
||||
* - Throws errors for invalid payloads or failed API calls.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ReturnCanReturnService {
|
||||
#logger = logger();
|
||||
|
||||
#receiptService = inject(ReceiptService);
|
||||
|
||||
/**
|
||||
* Determines if a return process can proceed.
|
||||
*
|
||||
* @param returnProcess - The return process object to evaluate.
|
||||
* @returns A promise resolving to a CanReturn result or undefined if the process should continue.
|
||||
*/
|
||||
async canReturn(returnProcess: ReturnProcess): Promise<CanReturn | undefined>;
|
||||
/**
|
||||
* Determines if a return can proceed based on mapped receipt values.
|
||||
*
|
||||
* @param returnValues - The mapped return receipt values.
|
||||
* @returns A promise resolving to a CanReturn result.
|
||||
*/
|
||||
async canReturn(returnValues: ReturnReceiptValues): Promise<CanReturn>;
|
||||
|
||||
/**
|
||||
* Determines if a return can proceed, accepting either a ReturnProcess or ReturnReceiptValues.
|
||||
*
|
||||
* @param input - The return process or mapped receipt values.
|
||||
* @returns A promise resolving to a CanReturn result or undefined if the process should continue.
|
||||
* @throws Error if payload validation fails or the backend call fails.
|
||||
*/
|
||||
@memorize()
|
||||
async canReturn(
|
||||
returnProcess: ReturnProcess,
|
||||
input: ReturnProcess | ReturnReceiptValues,
|
||||
): Promise<CanReturn | undefined> {
|
||||
let data: ReturnReceiptValues | undefined = undefined;
|
||||
|
||||
if (isReturnProcessTypeGuard(input)) {
|
||||
data = this._canReturnFromReturnProcess(input);
|
||||
} else {
|
||||
data = this._canReturnFromReturnReceiptValues(input);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return undefined; // Prozess soll weitergehen, daher kein Error
|
||||
}
|
||||
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
this.#receiptService
|
||||
.ReceiptCanReturn(data as ReturnReceiptValuesDTO)
|
||||
.pipe(
|
||||
debounceTime(50),
|
||||
map((res) => res as CanReturn),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`ReceiptCanReturn failed: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a ReturnProcess to ReturnReceiptValues and validates the payload.
|
||||
*
|
||||
* @param returnProcess - The return process to map and validate.
|
||||
* @returns The validated ReturnReceiptValues, or undefined if questions are unanswered.
|
||||
* @throws Error if payload validation fails.
|
||||
*/
|
||||
private _canReturnFromReturnProcess(
|
||||
returnProcess: ReturnProcess,
|
||||
): ReturnReceiptValues | undefined {
|
||||
if (!returnProcess) {
|
||||
return undefined;
|
||||
throw new Error('No return process provided');
|
||||
}
|
||||
|
||||
const questions = getReturnProcessQuestions(returnProcess);
|
||||
|
||||
if (!questions) {
|
||||
return undefined;
|
||||
return undefined; // Prozess soll weitergehen, daher kein Error
|
||||
}
|
||||
|
||||
const allQuestionsAnswered = allReturnProcessQuestionsAnswered({
|
||||
@@ -40,7 +108,7 @@ export class ReturnCanReturnService {
|
||||
});
|
||||
|
||||
if (!allQuestionsAnswered) {
|
||||
return undefined;
|
||||
return undefined; // Prozess soll weitergehen, daher kein Error
|
||||
}
|
||||
|
||||
const returnReceiptValues = returnReceiptValuesMapping(
|
||||
@@ -51,25 +119,29 @@ export class ReturnCanReturnService {
|
||||
ReturnReceiptValuesSchema.safeParse(returnReceiptValues);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
this.#logger.error(
|
||||
'CanReturn Payload validation failed',
|
||||
parsedPayload.error,
|
||||
);
|
||||
return undefined;
|
||||
throw new Error('CanReturn Payload validation failed');
|
||||
}
|
||||
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
this.#receiptService
|
||||
.ReceiptCanReturn(parsedPayload.data as ReturnReceiptValuesDTO)
|
||||
.pipe(
|
||||
debounceTime(50),
|
||||
map((res) => res as CanReturn),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
this.#logger.error('ReceiptCanReturn failed', error);
|
||||
return undefined;
|
||||
return parsedPayload.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided ReturnReceiptValues payload.
|
||||
*
|
||||
* @param returnReceiptValues - The values to validate.
|
||||
* @returns The validated ReturnReceiptValues.
|
||||
* @throws Error if payload validation fails.
|
||||
*/
|
||||
private _canReturnFromReturnReceiptValues(
|
||||
returnReceiptValues: ReturnReceiptValues,
|
||||
): ReturnReceiptValues | undefined {
|
||||
const parsedPayload =
|
||||
ReturnReceiptValuesSchema.safeParse(returnReceiptValues);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error('CanReturn Payload validation failed');
|
||||
}
|
||||
|
||||
return parsedPayload.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { FetchReturnDetails, FetchReturnDetailsSchema } from './schemas';
|
||||
import {
|
||||
FetchReturnDetails,
|
||||
FetchReturnDetailsSchema,
|
||||
ReturnReceiptValues,
|
||||
} from './schemas';
|
||||
import { map, Observable, throwError } from 'rxjs';
|
||||
import { ReceiptService } from '@generated/swagger/oms-api';
|
||||
import { Receipt } from './models';
|
||||
import { Receipt, ReceiptItem } from './models';
|
||||
import { CategoryQuestions } from './questions';
|
||||
import { KeyValue } from '@angular/common';
|
||||
import { ReturnCanReturnService } from './return-can-return.service';
|
||||
|
||||
/**
|
||||
* Service responsible for fetching return details for a given receipt.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ReturnDetailsService {
|
||||
#receiptService = inject(ReceiptService);
|
||||
#returnCanReturnService = inject(ReturnCanReturnService);
|
||||
|
||||
/**
|
||||
* Determines if a specific receipt item can be returned for a given category.
|
||||
*
|
||||
* @param params - The parameters for the return check.
|
||||
* @param params.item - The receipt item to check.
|
||||
* @param params.category - The product category to check against.
|
||||
* @returns A promise resolving to the result of the canReturn check.
|
||||
*/
|
||||
async canReturn({ item, category }: { item: ReceiptItem; category: string }) {
|
||||
const returnReceiptValues: ReturnReceiptValues = {
|
||||
quantity: item.quantity.quantity,
|
||||
receiptItem: {
|
||||
id: item.id,
|
||||
},
|
||||
category,
|
||||
};
|
||||
|
||||
return await this.#returnCanReturnService.canReturn(returnReceiptValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available product categories that have defined question sets.
|
||||
|
||||
@@ -85,11 +85,18 @@
|
||||
[attr.data-which]="i.product.ean"
|
||||
/>
|
||||
</ui-checkbox>
|
||||
} @else if (showDropdownLoading()) {
|
||||
<ui-icon-button
|
||||
[pending]="true"
|
||||
[color]="'tertiary'"
|
||||
data-what="load-spinner"
|
||||
data-which="can-return"
|
||||
></ui-icon-button>
|
||||
}
|
||||
</div>
|
||||
</ui-item-row>
|
||||
|
||||
@if (!canReturn()) {
|
||||
@if (!canReturnReceiptItem()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex items-center self-end gap-1 pb-6"
|
||||
>
|
||||
|
||||
@@ -10,14 +10,22 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
signal,
|
||||
WritableSignal,
|
||||
} from '@angular/core';
|
||||
import { logger, provideLoggerContext } from '@isa/core/logging';
|
||||
import { isaActionClose, ProductFormatIconGroup } from '@isa/icons';
|
||||
import { ReceiptItem } from '@isa/oms/data-access';
|
||||
import {
|
||||
CanReturn,
|
||||
ReceiptItem,
|
||||
ReturnDetailsService,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
CheckboxComponent,
|
||||
DropdownButtonComponent,
|
||||
@@ -26,27 +34,6 @@ import {
|
||||
import { ItemRowComponent } from '@isa/ui/item-rows';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
|
||||
/**
|
||||
* Component to display a single item within an order group for the return details feature.
|
||||
* It shows item details like image, name, price, and allows selection if the item is returnable.
|
||||
*
|
||||
* This component is responsible for:
|
||||
* - Displaying product information including image, name, and price
|
||||
* - Showing return eligibility status of the item
|
||||
* - Handling selection of returnable items
|
||||
* - Managing product category assignment for items with unknown categories
|
||||
* - Providing category change functionality through dropdown options
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <oms-feature-return-details-order-group-item
|
||||
* [item]="receiptItem"
|
||||
* [availableCategories]="categoryOptions"
|
||||
* [(selected)]="isItemSelected"
|
||||
* (changeCategory)="handleCategoryChange($event)">
|
||||
* </oms-feature-return-details-order-group-item>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'oms-feature-return-details-order-group-item',
|
||||
templateUrl: './return-details-order-group-item.component.html',
|
||||
@@ -58,13 +45,17 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
ProductImageDirective,
|
||||
CheckboxComponent,
|
||||
NgIconComponent,
|
||||
IconButtonComponent,
|
||||
DatePipe,
|
||||
CurrencyPipe,
|
||||
LowerCasePipe,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
],
|
||||
providers: [provideIcons({ ...ProductFormatIconGroup, isaActionClose })],
|
||||
providers: [
|
||||
provideIcons({ ...ProductFormatIconGroup, isaActionClose }),
|
||||
provideLoggerContext({ component: 'ReturnDetailsOrderGroupItemComponent' }),
|
||||
],
|
||||
})
|
||||
export class ReturnDetailsOrderGroupItemComponent {
|
||||
/**
|
||||
@@ -73,6 +64,17 @@ export class ReturnDetailsOrderGroupItemComponent {
|
||||
*/
|
||||
item = input.required<ReceiptItem>();
|
||||
|
||||
/**
|
||||
* Logger instance specific to this component context.
|
||||
* Used for structured logging of component events and errors.
|
||||
*/
|
||||
#logger = logger();
|
||||
|
||||
/**
|
||||
* Service for handling return details logic and API calls.
|
||||
*/
|
||||
#returnDetailsService = inject(ReturnDetailsService);
|
||||
|
||||
/**
|
||||
* Available product categories to select from when an item's category is unknown.
|
||||
* Presented as key-value pairs where the key is the category code and the value is the display name.
|
||||
@@ -80,86 +82,137 @@ export class ReturnDetailsOrderGroupItemComponent {
|
||||
availableCategories = input.required<KeyValue<string, string>[]>();
|
||||
|
||||
/**
|
||||
* Event emitted when the product category is changed.
|
||||
* Emits an object containing the item and the newly selected category.
|
||||
* Emits when the product category for the item is changed.
|
||||
* Payload contains the updated item and the selected category.
|
||||
*/
|
||||
changeCategory = output<{ item: ReceiptItem; category: string }>();
|
||||
|
||||
/**
|
||||
* Two-way binding for the selection state of the item.
|
||||
* When true, the item is selected for return.
|
||||
* Indicates whether the item is currently selected for return.
|
||||
*/
|
||||
selected = model(false);
|
||||
|
||||
/**
|
||||
* Controls the visibility of the category dropdown menu.
|
||||
* Set to true automatically for items with 'unknown' category.
|
||||
* Controls the visibility of the category dropdown.
|
||||
* Set to true if the product category is unknown and the item is returnable.
|
||||
*/
|
||||
readonly showDropdown = signal(false);
|
||||
|
||||
/**
|
||||
* Determines if the item can be selected for return.
|
||||
* An item is selectable if it is returnable and has a known product category.
|
||||
* Controls the visibility of the category dropdown loading spinner.
|
||||
*/
|
||||
readonly selectable = computed(() => {
|
||||
const productCategory = this.getProductCategory();
|
||||
const canReturn = this.canReturn();
|
||||
return canReturn && productCategory !== 'unknown';
|
||||
});
|
||||
readonly showDropdownLoading = signal(false);
|
||||
|
||||
/**
|
||||
* Computes whether the item can be returned based on its action properties.
|
||||
* Evaluates the 'canReturn' action property of the item.
|
||||
* @returns True if the item can be returned, false otherwise.
|
||||
* Indicates whether the item is selectable for return.
|
||||
* This signal is set to true if the item is eligible for return and has a known product category.
|
||||
* It is managed reactively based on the item's return eligibility and category state.
|
||||
*/
|
||||
canReturn = computed(() => {
|
||||
return this.item()?.actions?.some(
|
||||
selectable = signal(false);
|
||||
|
||||
/**
|
||||
* Computes whether the item can be returned.
|
||||
* Prefers the endpoint result if available, otherwise checks the item's actions.
|
||||
*/
|
||||
canReturnReceiptItem = computed(() => {
|
||||
const canReturn = this.canReturn()?.result;
|
||||
const canReturnReceiptItem = this.item()?.actions?.some(
|
||||
(a) => a.key === 'canReturn' && coerceBooleanProperty(a.value),
|
||||
);
|
||||
return canReturn ?? canReturnReceiptItem; // Endpoint Result (if existing) overrules item result
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the product category from the item's features.
|
||||
* Falls back to 'unknown' if no category is defined.
|
||||
* @returns The product category string.
|
||||
* Retrieves the product category for the item.
|
||||
* Returns 'unknown' if the category is not set.
|
||||
*/
|
||||
getProductCategory = computed(() => {
|
||||
return this.item()?.features?.['category'] || 'unknown';
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the message explaining whether an item can be returned.
|
||||
* This message is found in the description of the 'canReturn' action.
|
||||
* @returns The descriptive message about return eligibility.
|
||||
* Computes the message explaining the return eligibility.
|
||||
* Prefers the endpoint message if available, otherwise uses the item's action description.
|
||||
*/
|
||||
canReturnMessage = computed(() => {
|
||||
return this.item()?.actions?.find((a) => a.key === 'canReturn')
|
||||
?.description;
|
||||
const canReturnMessage = this.canReturn()?.message;
|
||||
const canReturnMessageOnReceiptItem = this.item()?.actions?.find(
|
||||
(a) => a.key === 'canReturn',
|
||||
)?.description;
|
||||
return canReturnMessage ?? canReturnMessageOnReceiptItem; // Endpoint Message (if existing) overrules item message
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up an effect to automatically show the category dropdown
|
||||
* when the item has an 'unknown' category.
|
||||
* Holds the latest result of the canReturn API call for this item.
|
||||
*/
|
||||
canReturn: WritableSignal<CanReturn | undefined> = signal(undefined);
|
||||
|
||||
/**
|
||||
* Initializes the component's reactive state management.
|
||||
*
|
||||
* This constructor sets up an effect that observes changes to the product category
|
||||
* and the return eligibility of the item. It ensures that:
|
||||
* - The category dropdown is shown if the product category is 'unknown' and the item is returnable.
|
||||
* - The item is only selectable if it is returnable and has a known product category.
|
||||
*
|
||||
* This logic enforces UI consistency and prevents invalid selection states.
|
||||
*
|
||||
* @remarks
|
||||
* - The effect is automatically disposed with the component's lifecycle.
|
||||
* - All state changes are performed via Angular signals for optimal reactivity.
|
||||
*/
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.getProductCategory() === 'unknown' && this.canReturn()) {
|
||||
const productCategory = this.getProductCategory();
|
||||
const canReturnReceiptItem = this.canReturnReceiptItem();
|
||||
|
||||
if (productCategory === 'unknown' && canReturnReceiptItem) {
|
||||
this.showDropdown.set(true);
|
||||
}
|
||||
|
||||
const isSelectable =
|
||||
canReturnReceiptItem && productCategory !== 'unknown';
|
||||
|
||||
if (!isSelectable) {
|
||||
this.selectable.set(false);
|
||||
} else {
|
||||
this.selectable.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the product category for the current item.
|
||||
* Emits the changeCategory event with the item and new category.
|
||||
* If no category is provided, defaults to 'unknown'.
|
||||
* Sets the product category for the item and checks if it can be returned.
|
||||
* Emits the changeCategory event and updates the canReturn signal.
|
||||
* Logs errors with context if the operation fails.
|
||||
*
|
||||
* @param category The new category to assign to the product
|
||||
* @param category - The new category code to assign to the item, or undefined to reset to 'unknown'.
|
||||
*/
|
||||
setProductCategory(category: string | undefined) {
|
||||
this.changeCategory.emit({
|
||||
async setProductCategory(category: string | undefined) {
|
||||
const itemToUpdate = {
|
||||
item: this.item(),
|
||||
category: category || 'unknown',
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
this.showDropdownLoading.set(true);
|
||||
this.canReturn.set(undefined);
|
||||
this.selectable.set(false);
|
||||
|
||||
const canReturn =
|
||||
await this.#returnDetailsService.canReturn(itemToUpdate);
|
||||
this.canReturn.set(canReturn);
|
||||
|
||||
this.changeCategory.emit(itemToUpdate);
|
||||
this.showDropdownLoading.set(false);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to setProductCategory', error, {
|
||||
itemId: this.item().id,
|
||||
category,
|
||||
});
|
||||
this.canReturn.set(undefined);
|
||||
this.showDropdownLoading.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,11 @@ export class ReturnDetailsOrderGroupComponent {
|
||||
selectedItems = model<ReceiptItem[]>([]);
|
||||
|
||||
selectableItems = computed(() => {
|
||||
return this.items().filter((item) =>
|
||||
item.actions?.some(
|
||||
(a) => a.key === 'canReturn' && coerceBooleanProperty(a.value),
|
||||
),
|
||||
return this.items().filter(
|
||||
(item) =>
|
||||
item.actions?.some(
|
||||
(a) => a.key === 'canReturn' && coerceBooleanProperty(a.value),
|
||||
) && item?.features?.['category'] !== 'unknown',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -118,7 +118,6 @@ export class ReturnDetailsComponent {
|
||||
item: ReceiptItem;
|
||||
category: string;
|
||||
}) {
|
||||
// TODO: Can-Return request
|
||||
this.#returnDetailsStore.updateProductCategoryForItem({
|
||||
receiptId: this.itemId(),
|
||||
itemId: item.id,
|
||||
|
||||
@@ -42,5 +42,14 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
} @else if (canReturnReceipt === undefined && eligible) {
|
||||
<div class="w-full flex items-center justify-end">
|
||||
<ui-icon-button
|
||||
[pending]="true"
|
||||
[color]="'tertiary'"
|
||||
data-what="load-spinner"
|
||||
data-which="can-return"
|
||||
></ui-icon-button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import { isaActionCheck, isaActionClose } from '@isa/icons';
|
||||
import { ReturnProcessQuestionsComponent } from '../return-process-questions/return-process-questions.component';
|
||||
import { ProgressBarComponent } from '@isa/ui/progress-bar';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
import { logger, provideLoggerContext } from '@isa/core/logging';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
/**
|
||||
* Component that displays a single return process item with its associated questions and progress.
|
||||
@@ -43,9 +45,13 @@ import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
NgIconComponent,
|
||||
ReturnProcessQuestionsComponent,
|
||||
ProgressBarComponent,
|
||||
IconButtonComponent,
|
||||
ReturnProductInfoComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionCheck, isaActionClose })],
|
||||
providers: [
|
||||
provideIcons({ isaActionCheck, isaActionClose }),
|
||||
provideLoggerContext({ component: 'ReturnProcessItemComponent' }),
|
||||
],
|
||||
})
|
||||
export class ReturnProcessItemComponent {
|
||||
EligibleForReturnState = EligibleForReturnState;
|
||||
@@ -53,6 +59,9 @@ export class ReturnProcessItemComponent {
|
||||
#returnCanReturnService = inject(ReturnCanReturnService);
|
||||
returnProcessStore = inject(ReturnProcessStore);
|
||||
|
||||
/** Logger instance specific to this component context. */
|
||||
#logger = logger();
|
||||
|
||||
/** The unique identifier for the return process */
|
||||
returnProcessId = input.required<number>();
|
||||
|
||||
@@ -68,6 +77,16 @@ export class ReturnProcessItemComponent {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal holding the backend validation result for whether the current return process
|
||||
* can be returned. Initially undefined, it is asynchronously set after backend validation.
|
||||
*
|
||||
* @type {WritableSignal<CanReturn | undefined>}
|
||||
* @see ReturnCanReturnService.canReturn
|
||||
* @remarks
|
||||
* - This signal is reset to undefined before each backend check.
|
||||
* - The value is updated with the backend response for the current return process.
|
||||
*/
|
||||
canReturn: WritableSignal<CanReturn | undefined> = signal(undefined);
|
||||
|
||||
/**
|
||||
@@ -114,6 +133,18 @@ export class ReturnProcessItemComponent {
|
||||
return canReturnMessage ?? eligibleForReturnReason; // Fallback to eligibleForReturnReason if canReturnMessage is not available
|
||||
});
|
||||
|
||||
/**
|
||||
* Initializes the component and sets up a reactive effect to validate
|
||||
* the return process eligibility with the backend whenever the process changes.
|
||||
*
|
||||
* - Resets the `canReturn` signal to `undefined` before each backend check.
|
||||
* - Asynchronously updates `canReturn` with the backend response.
|
||||
* - Ensures backend validation is always in sync with the current return process.
|
||||
*
|
||||
* @remarks
|
||||
* This approach leverages Angular signals and effects for local state management,
|
||||
* following workspace guidelines for clean, reactive, and maintainable code.
|
||||
*/
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const returnProcess = this.returnProcess();
|
||||
@@ -121,10 +152,18 @@ export class ReturnProcessItemComponent {
|
||||
untracked(async () => {
|
||||
if (returnProcess) {
|
||||
this.canReturn.set(undefined);
|
||||
const canReturnResponse =
|
||||
await this.#returnCanReturnService.canReturn(returnProcess);
|
||||
|
||||
this.canReturn.set(canReturnResponse);
|
||||
try {
|
||||
const canReturnResponse =
|
||||
await this.#returnCanReturnService.canReturn(returnProcess);
|
||||
this.canReturn.set(canReturnResponse);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to validate return process', error, {
|
||||
returnProcessId: returnProcess.id,
|
||||
});
|
||||
this.canReturn.set(undefined);
|
||||
}
|
||||
} else {
|
||||
this.canReturn.set(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronLeft } from '@isa/icons';
|
||||
import {
|
||||
CanReturn,
|
||||
EligibleForReturnState,
|
||||
ReturnCanReturnService,
|
||||
ReturnProcess,
|
||||
@@ -120,28 +119,61 @@ export class ReturnProcessComponent {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal indicating whether all return processes can be returned.
|
||||
* Updated asynchronously based on the result of canReturn checks for each process.
|
||||
*/
|
||||
canReturn: WritableSignal<boolean> = signal(false);
|
||||
|
||||
/**
|
||||
* Initializes the component and sets up an effect to determine if all return processes
|
||||
* are eligible for return. The effect observes changes to `returnProcesses` and updates
|
||||
* the `canReturn` signal accordingly.
|
||||
*
|
||||
* The logic:
|
||||
* - Resets `canReturn` to undefined on each effect run.
|
||||
* - For each process, asynchronously checks if it can be returned using `ReturnCanReturnService`.
|
||||
* - If all processes return a positive result (`result === true`), sets `canReturn` to true.
|
||||
* - Handles empty or missing process lists gracefully.
|
||||
*
|
||||
* @remarks
|
||||
* - Uses `untracked` to avoid tracking the async block in the effect's dependency graph.
|
||||
* - Ensures type safety and avoids unnecessary state updates.
|
||||
* - Logs errors if the canReturn check fails for any process.
|
||||
*/
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const processes = this.returnProcesses();
|
||||
this.canReturn.set(false);
|
||||
|
||||
// Avoid tracking the async block in the effect's dependency graph
|
||||
untracked(async () => {
|
||||
if (processes && processes.length > 0) {
|
||||
const canReturnProcesses = [];
|
||||
for (const returnProcess of processes) {
|
||||
if (!Array.isArray(processes) || processes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canReturnResults: boolean[] = [];
|
||||
for (const returnProcess of processes) {
|
||||
try {
|
||||
const canReturnResponse =
|
||||
await this.#returnCanReturnService.canReturn(returnProcess);
|
||||
|
||||
if (canReturnResponse) {
|
||||
canReturnProcesses.push(canReturnResponse);
|
||||
}
|
||||
canReturnResults.push(Boolean(canReturnResponse?.result));
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to check canReturn for process', error, {
|
||||
processId: returnProcess.processId,
|
||||
});
|
||||
canReturnResults.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (canReturnProcesses.every((p) => p.result)) {
|
||||
this.canReturn.set(true);
|
||||
}
|
||||
if (
|
||||
canReturnResults.length === processes.length &&
|
||||
canReturnResults.every(Boolean)
|
||||
) {
|
||||
this.canReturn.set(true);
|
||||
} else {
|
||||
this.canReturn.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user