mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
8 Commits
7200eaefbf
...
feature/53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9a80a192e | ||
|
|
3d82e7f0af | ||
|
|
3101e8e8e0 | ||
|
|
a3415e450d | ||
|
|
de3edaa0f9 | ||
|
|
964a6026a0 | ||
|
|
83ad5f526e | ||
|
|
ccc5285602 |
@@ -1,6 +1,5 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { AvailabilityService as GeneratedAvailabilityService } from '@generated/swagger/availability-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import { LogisticianService, Logistician } from '@isa/oms/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
// TODO: [Next Sprint - Architectural] Abstract cross-domain dependency
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
/**
|
||||
* Creates a unique key for an item based on EAN, destination, and orderItemType.
|
||||
* Creates a unique key for an item based on EAN, targetBranchId, and orderItemType.
|
||||
* Items are only considered identical if all three match.
|
||||
*/
|
||||
export const getItemKey = (item: ShoppingCartItem): string => {
|
||||
const ean = item.product.ean ?? 'no-ean';
|
||||
const destinationId = item.destination?.data?.id ?? 'no-destination';
|
||||
const targetBranchId =
|
||||
item.destination?.data?.targetBranch?.id ?? 'no-target-branch-id';
|
||||
const orderType = getOrderTypeFeature(item.features) ?? 'no-orderType';
|
||||
return `${ean}|${destinationId}|${orderType}`;
|
||||
return `${ean}|${targetBranchId}|${orderType}`;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { StoreCheckoutBranchService } from '@generated/swagger/checkout-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
|
||||
@@ -14,20 +17,20 @@ export class BranchService {
|
||||
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
|
||||
@InFlight()
|
||||
async fetchBranches(abortSignal?: AbortSignal): Promise<Branch[]> {
|
||||
let req$ = this.#branchService.StoreCheckoutBranchGetBranches({});
|
||||
let req$ = this.#branchService
|
||||
.StoreCheckoutBranchGetBranches({})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Branch[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch branches', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Branch[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
PrintOrderConfirmation,
|
||||
PrintOrderConfirmationSchema,
|
||||
} from '../schemas';
|
||||
import { ResponseArgsError } from '@isa/common/data-access';
|
||||
import { catchResponseArgsErrorPipe } from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@@ -18,16 +18,19 @@ export class CheckoutPrintService {
|
||||
): Promise<ResponseArgs> {
|
||||
const parsed = PrintOrderConfirmationSchema.parse(params);
|
||||
|
||||
const req$ = this.#omsPrintService.OMSPrintAbholscheinById(parsed);
|
||||
const req$ = this.#omsPrintService
|
||||
.OMSPrintAbholscheinById(parsed)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to print order confirmation', err);
|
||||
throw err;
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to print order confirmation', error, () => ({
|
||||
printer: parsed.printer,
|
||||
orderIds: parsed.data,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,11 @@ import {
|
||||
StoreCheckoutPayerService,
|
||||
StoreCheckoutPaymentService,
|
||||
DestinationDTO,
|
||||
BuyerDTO,
|
||||
PayerDTO,
|
||||
AvailabilityDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
|
||||
import {
|
||||
EntityContainer,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
catchResponseArgsErrorPipe,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
@@ -29,7 +25,6 @@ import {
|
||||
PaymentType,
|
||||
} from '../schemas';
|
||||
import {
|
||||
Order,
|
||||
OrderOptionsAnalysis,
|
||||
CustomerTypeAnalysis,
|
||||
Checkout,
|
||||
@@ -155,7 +150,6 @@ export class CheckoutService {
|
||||
this.#logger.debug('Creating or refreshing checkout');
|
||||
const initialCheckout = await this.refreshCheckout(
|
||||
validated.shoppingCartId,
|
||||
abortSignal,
|
||||
);
|
||||
const checkoutId = initialCheckout.id;
|
||||
|
||||
@@ -169,7 +163,6 @@ export class CheckoutService {
|
||||
await this.updateDestinationsForCustomer(
|
||||
validated.shoppingCartId,
|
||||
validated.customerFeatures,
|
||||
abortSignal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +181,6 @@ export class CheckoutService {
|
||||
validated.shoppingCartId,
|
||||
shoppingCart.items,
|
||||
validated.specialComment,
|
||||
abortSignal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,26 +201,25 @@ export class CheckoutService {
|
||||
|
||||
// Step 9: Set buyer on checkout
|
||||
this.#logger.debug('Setting buyer on checkout');
|
||||
await this.setBuyerOnCheckout(checkoutId, validated.buyer, abortSignal);
|
||||
await this.setBuyerOnCheckout(checkoutId, validated.buyer);
|
||||
|
||||
// Step 10: Set notification channels
|
||||
this.#logger.debug('Setting notification channels');
|
||||
await this.setNotificationChannelsOnCheckout(
|
||||
checkoutId,
|
||||
validated.notificationChannels ?? 0,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
// Step 11: Set payer (conditional)
|
||||
if (needsPayer && validated.payer) {
|
||||
this.#logger.debug('Setting payer on checkout');
|
||||
await this.setPayerOnCheckout(checkoutId, validated.payer, abortSignal);
|
||||
await this.setPayerOnCheckout(checkoutId, validated.payer);
|
||||
}
|
||||
|
||||
// Step 12: Set payment type based on order types
|
||||
const paymentType = this.determinePaymentType(orderOptions);
|
||||
this.#logger.debug('Setting payment type');
|
||||
await this.setPaymentTypeOnCheckout(checkoutId, paymentType, abortSignal);
|
||||
await this.setPaymentTypeOnCheckout(checkoutId, paymentType);
|
||||
|
||||
// Step 13: Update destination shipping addresses (if delivery or download)
|
||||
// Refresh checkout only when we need the destinations data
|
||||
@@ -240,17 +231,13 @@ export class CheckoutService {
|
||||
validated.shippingAddress
|
||||
) {
|
||||
this.#logger.debug('Refreshing checkout to get destinations');
|
||||
const checkout = await this.refreshCheckout(
|
||||
validated.shoppingCartId,
|
||||
abortSignal,
|
||||
);
|
||||
const checkout = await this.refreshCheckout(validated.shoppingCartId);
|
||||
|
||||
this.#logger.debug('Updating destination shipping addresses');
|
||||
await this.updateDestinationShippingAddresses(
|
||||
checkoutId,
|
||||
checkout,
|
||||
validated.shippingAddress,
|
||||
abortSignal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -330,25 +317,22 @@ export class CheckoutService {
|
||||
private async updateDestinationsForCustomer(
|
||||
shoppingCartId: number,
|
||||
customerFeatures: Record<string, string>,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
let req$ =
|
||||
this.#shoppingCartService.StoreCheckoutShoppingCartSetLogisticianOnDestinationsByBuyer(
|
||||
{
|
||||
shoppingCartId,
|
||||
payload: { customerFeatures },
|
||||
},
|
||||
const req$ = this.#shoppingCartService
|
||||
.StoreCheckoutShoppingCartSetLogisticianOnDestinationsByBuyer({
|
||||
shoppingCartId,
|
||||
payload: { customerFeatures },
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
try {
|
||||
await firstValueFrom(req$);
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to update destinations for customer',
|
||||
error,
|
||||
() => ({ shoppingCartId }),
|
||||
);
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to update destinations for customer', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -360,31 +344,30 @@ export class CheckoutService {
|
||||
shoppingCartId: number,
|
||||
items: EntityContainer<ShoppingCartItem>[],
|
||||
comment: string,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
// Update all items in parallel
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
if (!item.id) return;
|
||||
|
||||
let req$ =
|
||||
this.#shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
|
||||
{
|
||||
const req$ = this.#shoppingCartService
|
||||
.StoreCheckoutShoppingCartUpdateShoppingCartItem({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId: item.id,
|
||||
values: { specialComment: comment },
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
try {
|
||||
await firstValueFrom(req$);
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to set special comment on item',
|
||||
error,
|
||||
() => ({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId: item.id,
|
||||
values: { specialComment: comment },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to set special comment on item');
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
@@ -394,27 +377,22 @@ export class CheckoutService {
|
||||
/**
|
||||
* Refreshes checkout to get latest state.
|
||||
*/
|
||||
private async refreshCheckout(
|
||||
shoppingCartId: number,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Checkout> {
|
||||
let req$ = this.#storeCheckoutService.StoreCheckoutCreateOrRefreshCheckout({
|
||||
shoppingCartId,
|
||||
});
|
||||
private async refreshCheckout(shoppingCartId: number): Promise<Checkout> {
|
||||
const req$ = this.#storeCheckoutService
|
||||
.StoreCheckoutCreateOrRefreshCheckout({
|
||||
shoppingCartId,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to refresh checkout', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Checkout;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to refresh checkout', error, () => ({
|
||||
shoppingCartId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Checkout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,7 +450,7 @@ export class CheckoutService {
|
||||
const availabilityDTO =
|
||||
AvailabilityAdapter.fromAvailabilityApi(availability);
|
||||
|
||||
let updateReq$ =
|
||||
const updateReq$ =
|
||||
this.#shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability(
|
||||
{
|
||||
shoppingCartId,
|
||||
@@ -481,10 +459,6 @@ export class CheckoutService {
|
||||
},
|
||||
);
|
||||
|
||||
if (abortSignal) {
|
||||
updateReq$ = updateReq$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const updateRes = await firstValueFrom(updateReq$);
|
||||
|
||||
if (updateRes.error) {
|
||||
@@ -582,7 +556,7 @@ export class CheckoutService {
|
||||
};
|
||||
}
|
||||
|
||||
let req$ =
|
||||
const req$ =
|
||||
this.#shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
|
||||
{
|
||||
shoppingCartId,
|
||||
@@ -591,10 +565,6 @@ export class CheckoutService {
|
||||
},
|
||||
);
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
@@ -618,26 +588,23 @@ export class CheckoutService {
|
||||
private async setBuyerOnCheckout(
|
||||
checkoutId: number,
|
||||
buyerDTO: Buyer,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Checkout> {
|
||||
let req$ = this.#buyerService.StoreCheckoutBuyerSetBuyerPOST({
|
||||
checkoutId,
|
||||
buyerDTO,
|
||||
});
|
||||
const req$ = this.#buyerService
|
||||
.StoreCheckoutBuyerSetBuyerPOST({
|
||||
checkoutId,
|
||||
buyerDTO,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to set buyer', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Checkout;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to set buyer on checkout', error, () => ({
|
||||
checkoutId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Checkout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -646,26 +613,23 @@ export class CheckoutService {
|
||||
private async setPayerOnCheckout(
|
||||
checkoutId: number,
|
||||
payer: Payer,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Checkout> {
|
||||
let req$ = this.#payerService.StoreCheckoutPayerSetPayerPOST({
|
||||
checkoutId,
|
||||
payerDTO: payer,
|
||||
});
|
||||
const req$ = this.#payerService
|
||||
.StoreCheckoutPayerSetPayerPOST({
|
||||
checkoutId,
|
||||
payerDTO: payer,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to set payer', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Checkout;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to set payer on checkout', error, () => ({
|
||||
checkoutId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Checkout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -674,26 +638,25 @@ export class CheckoutService {
|
||||
private async setNotificationChannelsOnCheckout(
|
||||
checkoutId: number,
|
||||
channels: NotificationChannel,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Checkout> {
|
||||
let req$ = this.#storeCheckoutService.StoreCheckoutSetNotificationChannels({
|
||||
checkoutId,
|
||||
notificationChannel: channels,
|
||||
});
|
||||
const req$ = this.#storeCheckoutService
|
||||
.StoreCheckoutSetNotificationChannels({
|
||||
checkoutId,
|
||||
notificationChannel: channels,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to set notification channels', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Checkout;
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to set notification channels on checkout',
|
||||
error,
|
||||
() => ({ checkoutId }),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Checkout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -702,26 +665,25 @@ export class CheckoutService {
|
||||
private async setPaymentTypeOnCheckout(
|
||||
checkoutId: number,
|
||||
paymentType: PaymentType,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Checkout> {
|
||||
let req$ = this.#paymentService.StoreCheckoutPaymentSetPaymentType({
|
||||
checkoutId,
|
||||
paymentType: paymentType,
|
||||
});
|
||||
const req$ = this.#paymentService
|
||||
.StoreCheckoutPaymentSetPaymentType({
|
||||
checkoutId,
|
||||
paymentType: paymentType,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to set payment type', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Checkout;
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to set payment type on checkout',
|
||||
error,
|
||||
() => ({ checkoutId, paymentType }),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Checkout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -731,7 +693,6 @@ export class CheckoutService {
|
||||
checkoutId: number,
|
||||
checkout: Checkout,
|
||||
shippingAddress: ShippingAddress,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
// Get shipping destinations (target 2 or 16)
|
||||
const destinations = filterDeliveryDestinations(
|
||||
@@ -751,21 +712,21 @@ export class CheckoutService {
|
||||
shippingAddress: { ...shippingAddress },
|
||||
};
|
||||
|
||||
let req$ = this.#storeCheckoutService.StoreCheckoutUpdateDestination({
|
||||
checkoutId,
|
||||
destinationId: dest.id,
|
||||
destinationDTO: updatedDestination,
|
||||
});
|
||||
const req$ = this.#storeCheckoutService
|
||||
.StoreCheckoutUpdateDestination({
|
||||
checkoutId,
|
||||
destinationId: dest.id,
|
||||
destinationDTO: updatedDestination,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to update destination');
|
||||
try {
|
||||
await firstValueFrom(req$);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to update destination', error, () => ({
|
||||
checkoutId,
|
||||
destinationId: dest.id,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '../schemas';
|
||||
import { RewardSelectionItem, ShoppingCart, ShoppingCartItem } from '../models';
|
||||
import {
|
||||
ResponseArgsError,
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
ensureCurrencyDefaults,
|
||||
} from '@isa/common/data-access';
|
||||
@@ -36,107 +36,105 @@ export class ShoppingCartService {
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
|
||||
async createShoppingCart(): Promise<ShoppingCart> {
|
||||
const req$ =
|
||||
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartCreateShoppingCart();
|
||||
const req$ = this.#storeCheckoutShoppingCartService
|
||||
.StoreCheckoutShoppingCartCreateShoppingCart()
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to create shopping cart', err);
|
||||
throw err;
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.Created,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to create shopping cart', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.Created,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
}
|
||||
|
||||
async getShoppingCart(
|
||||
shoppingCartId: number,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ShoppingCart | undefined> {
|
||||
let req$ =
|
||||
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartGetShoppingCart(
|
||||
{
|
||||
shoppingCartId,
|
||||
},
|
||||
);
|
||||
let req$ = this.#storeCheckoutShoppingCartService
|
||||
.StoreCheckoutShoppingCartGetShoppingCart({
|
||||
shoppingCartId,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch shopping cart', err);
|
||||
throw err;
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemUpdated,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to get shopping cart', error, () => ({
|
||||
shoppingCartId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemUpdated,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
}
|
||||
|
||||
async canAddItems(
|
||||
params: CanAddItemsToShoppingCartParams,
|
||||
): Promise<ItemsResult[]> {
|
||||
const parsed = CanAddItemsToShoppingCartParamsSchema.parse(params);
|
||||
const req$ =
|
||||
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartCanAddItems(
|
||||
{
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
payload: parsed.payload as ItemPayload[],
|
||||
},
|
||||
);
|
||||
const req$ = this.#storeCheckoutShoppingCartService
|
||||
.StoreCheckoutShoppingCartCanAddItems({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
payload: parsed.payload as ItemPayload[],
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as unknown as ItemsResult[];
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to check if items can be added to shopping cart',
|
||||
err,
|
||||
'Failed to check if items can be added',
|
||||
error,
|
||||
() => ({ shoppingCartId: parsed.shoppingCartId }),
|
||||
);
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as unknown as ItemsResult[];
|
||||
}
|
||||
|
||||
async addItem(params: AddItemToShoppingCartParams): Promise<ShoppingCart> {
|
||||
const parsed = AddItemToShoppingCartParamsSchema.parse(params);
|
||||
|
||||
const req$ =
|
||||
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartAddItemToShoppingCart(
|
||||
{
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
items: parsed.items as AddToShoppingCartDTO[],
|
||||
},
|
||||
const req$ = this.#storeCheckoutShoppingCartService
|
||||
.StoreCheckoutShoppingCartAddItemToShoppingCart({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
items: parsed.items as AddToShoppingCartDTO[],
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemAdded,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to add item to shopping cart', err);
|
||||
throw err;
|
||||
return res.result as ShoppingCart;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to add item to shopping cart', error, () => ({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemAdded,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
}
|
||||
|
||||
async updateItem(
|
||||
@@ -144,60 +142,62 @@ export class ShoppingCartService {
|
||||
): Promise<ShoppingCart> {
|
||||
const parsed = UpdateShoppingCartItemParamsSchema.parse(params);
|
||||
|
||||
const req$ =
|
||||
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
|
||||
{
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
shoppingCartItemId: parsed.shoppingCartItemId,
|
||||
values: parsed.values as UpdateShoppingCartItemDTO,
|
||||
},
|
||||
const req$ = this.#storeCheckoutShoppingCartService
|
||||
.StoreCheckoutShoppingCartUpdateShoppingCartItem({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
shoppingCartItemId: parsed.shoppingCartItemId,
|
||||
values: parsed.values as UpdateShoppingCartItemDTO,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemUpdated,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to update shopping cart item', err);
|
||||
throw err;
|
||||
return res.result as ShoppingCart;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to update shopping cart item', error, () => ({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
shoppingCartItemId: parsed.shoppingCartItemId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemUpdated,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
}
|
||||
|
||||
async removeItem(
|
||||
params: RemoveShoppingCartItemParams,
|
||||
): Promise<ShoppingCart> {
|
||||
const parsed = RemoveShoppingCartItemParamsSchema.parse(params);
|
||||
const req$ =
|
||||
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
|
||||
{
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
shoppingCartItemId: parsed.shoppingCartItemId,
|
||||
values: {
|
||||
quantity: 0,
|
||||
},
|
||||
const req$ = this.#storeCheckoutShoppingCartService
|
||||
.StoreCheckoutShoppingCartUpdateShoppingCartItem({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
shoppingCartItemId: parsed.shoppingCartItemId,
|
||||
values: {
|
||||
quantity: 0,
|
||||
},
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemRemoved,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to remove item from shopping cart', err);
|
||||
throw err;
|
||||
return res.result as ShoppingCart;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remove shopping cart item', error, () => ({
|
||||
shoppingCartId: parsed.shoppingCartId,
|
||||
shoppingCartItemId: parsed.shoppingCartItemId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#shoppingCartStream.pub(
|
||||
ShoppingCartEvent.ItemRemoved,
|
||||
res.result as ShoppingCart,
|
||||
'ShoppingCartService',
|
||||
);
|
||||
return res.result as ShoppingCart;
|
||||
}
|
||||
|
||||
// TODO: Code Kommentieren + Beschreiben
|
||||
|
||||
@@ -87,7 +87,8 @@ describe('SupplierService', () => {
|
||||
// Arrange
|
||||
const errorResponse = {
|
||||
result: null,
|
||||
error: { message: 'API Error', code: 500 },
|
||||
error: true,
|
||||
message: 'API Error',
|
||||
};
|
||||
|
||||
mockSupplierService.StoreCheckoutSupplierGetSuppliers.mockReturnValue(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { StoreCheckoutSupplierService } from '@generated/swagger/checkout-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
|
||||
@@ -32,37 +35,42 @@ export class SupplierService {
|
||||
async getTakeAwaySupplier(abortSignal?: AbortSignal): Promise<Supplier> {
|
||||
this.#logger.debug('Fetching take away supplier');
|
||||
|
||||
let req$ = this.#supplierService.StoreCheckoutSupplierGetSuppliers({});
|
||||
let req$ = this.#supplierService
|
||||
.StoreCheckoutSupplierGetSuppliers({})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch suppliers', error);
|
||||
const takeAwaySupplier = res.result?.find(
|
||||
(supplier) => supplier.supplierNumber === 'F',
|
||||
);
|
||||
|
||||
if (!takeAwaySupplier) {
|
||||
const notFoundError = new Error('Take away supplier (F) not found');
|
||||
this.#logger.error(
|
||||
'Take away supplier not found',
|
||||
notFoundError,
|
||||
() => ({
|
||||
availableSuppliers: res.result?.map((s) => s.supplierNumber),
|
||||
}),
|
||||
);
|
||||
throw notFoundError;
|
||||
}
|
||||
|
||||
this.#logger.debug('Take away supplier fetched', () => ({
|
||||
supplierId: takeAwaySupplier.id,
|
||||
supplierNumber: takeAwaySupplier.supplierNumber,
|
||||
}));
|
||||
|
||||
return takeAwaySupplier;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch take away supplier', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const takeAwaySupplier = res.result?.find(
|
||||
(supplier) => supplier.supplierNumber === 'F',
|
||||
);
|
||||
|
||||
if (!takeAwaySupplier) {
|
||||
const notFoundError = new Error('Take away supplier (F) not found');
|
||||
this.#logger.error('Take away supplier not found', notFoundError, () => ({
|
||||
availableSuppliers: res.result?.map((s) => s.supplierNumber),
|
||||
}));
|
||||
throw notFoundError;
|
||||
}
|
||||
|
||||
this.#logger.debug('Take away supplier fetched', () => ({
|
||||
supplierId: takeAwaySupplier.id,
|
||||
supplierNumber: takeAwaySupplier.supplierNumber,
|
||||
}));
|
||||
|
||||
return takeAwaySupplier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ export class RewardShoppingCartItemQuantityControlComponent {
|
||||
if (
|
||||
orderType === OrderTypeFeature.Delivery ||
|
||||
orderType === OrderTypeFeature.DigitalShipping ||
|
||||
orderType === OrderTypeFeature.B2BShipping
|
||||
orderType === OrderTypeFeature.B2BShipping ||
|
||||
orderType === OrderTypeFeature.Pickup
|
||||
) {
|
||||
return 999;
|
||||
}
|
||||
|
||||
@@ -37,11 +37,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (quantityControl.maxQuantity() < 2 && !isDownload()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>Geringer Bestand - Artikel holen vor Abschluss</div>
|
||||
</div>
|
||||
@if (!isDownload()) {
|
||||
@if (showLowStockMessage()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>{{ inStock() }} Exemplare sofort lieferbar</div>
|
||||
</div>
|
||||
} @else if (quantityControl.maxQuantity() < 2) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>Geringer Bestand - Artikel holen vor Abschluss</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,16 @@ export class RewardShoppingCartItemComponent {
|
||||
hasOrderTypeFeature(this.item().features, ['Download']),
|
||||
);
|
||||
|
||||
isAbholung = computed(() =>
|
||||
hasOrderTypeFeature(this.item().features, ['Abholung']),
|
||||
);
|
||||
|
||||
inStock = computed(() => this.item().availability?.inStock ?? 0);
|
||||
|
||||
showLowStockMessage = computed(() => {
|
||||
return this.isAbholung() && this.inStock() < 2;
|
||||
});
|
||||
|
||||
async updatePurchaseOption() {
|
||||
const shoppingCartItemId = this.itemId();
|
||||
const shoppingCartId = this.shoppingCartId();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:host {
|
||||
@apply text-isa-accent-red isa-text-body-2-bold flex flex-row gap-2 items-center;
|
||||
@apply text-isa-accent-red isa-text-body-2-bold flex flex-col gap-2 items-start;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@if (store.totalLoyaltyPointsNeeded() > store.customerRewardPoints()) {
|
||||
<ng-icon
|
||||
class="w-6 h-6 inline-flex items-center justify-center"
|
||||
size="1.5rem"
|
||||
name="isaOtherInfo"
|
||||
></ng-icon>
|
||||
<span>Lesepunkte reichen nicht für alle Artikel</span>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<ng-icon
|
||||
class="w-6 h-6 inline-flex items-center justify-center"
|
||||
size="1.5rem"
|
||||
name="isaOtherInfo"
|
||||
></ng-icon>
|
||||
<span>Lesepunkte reichen nicht für alle Artikel</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -41,10 +41,7 @@ export class RewardSelectionInputsComponent {
|
||||
|
||||
hasCorrectOrderType = computed(() => {
|
||||
const item = this.rewardSelectionItem().item;
|
||||
return hasOrderTypeFeature(item.features, [
|
||||
OrderTypeFeature.InStore,
|
||||
OrderTypeFeature.Pickup,
|
||||
]);
|
||||
return hasOrderTypeFeature(item.features, [OrderTypeFeature.InStore]);
|
||||
});
|
||||
|
||||
hasStock = computed(() => {
|
||||
|
||||
@@ -27,3 +27,16 @@
|
||||
|
||||
<lib-reward-selection-inputs></lib-reward-selection-inputs>
|
||||
</div>
|
||||
|
||||
@if (showLowStockMessage()) {
|
||||
<div
|
||||
class="flex flex-row gap-2 items-center text-isa-accent-red isa-text-body-2-bold"
|
||||
>
|
||||
<ng-icon
|
||||
class="w-6 h-6 inline-flex items-center justify-center"
|
||||
size="1.5rem"
|
||||
name="isaOtherInfo"
|
||||
></ng-icon>
|
||||
<span>{{ inStock() }} Exemplare sofort lieferbar</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
import { RewardSelectionInputsComponent } from './reward-selection-inputs/reward-selection-inputs.component';
|
||||
import { RewardSelectionItem } from '@isa/checkout/data-access';
|
||||
import {
|
||||
hasOrderTypeFeature,
|
||||
RewardSelectionItem,
|
||||
} from '@isa/checkout/data-access';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaOtherInfo } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-reward-selection-item',
|
||||
@@ -13,8 +23,24 @@ import { RewardSelectionItem } from '@isa/checkout/data-access';
|
||||
ProductImageDirective,
|
||||
ProductRouterLinkDirective,
|
||||
RewardSelectionInputsComponent,
|
||||
NgIcon,
|
||||
],
|
||||
providers: [provideIcons({ isaOtherInfo })],
|
||||
})
|
||||
export class RewardSelectionItemComponent {
|
||||
rewardSelectionItem = input.required<RewardSelectionItem>();
|
||||
|
||||
inStock = computed(
|
||||
() => this.rewardSelectionItem().item?.availability?.inStock ?? 0,
|
||||
);
|
||||
|
||||
isAbholung = computed(() =>
|
||||
hasOrderTypeFeature(this.rewardSelectionItem()?.item?.features, [
|
||||
'Abholung',
|
||||
]),
|
||||
);
|
||||
|
||||
showLowStockMessage = computed(() => {
|
||||
return this.isAbholung() && this.inStock() < 2;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,8 +34,15 @@ export const catchResponseArgsErrorPipe = <T>(): OperatorFunction<T, T> =>
|
||||
return throwError(() => err);
|
||||
}),
|
||||
mergeMap((response) => {
|
||||
if (isResponseArgs(response) && response.error === true) {
|
||||
return throwError(() => new ResponseArgsError(response));
|
||||
if (isResponseArgs(response)) {
|
||||
// Treat as error if error flag is true OR if invalidProperties has entries
|
||||
const hasInvalidProps =
|
||||
response.invalidProperties &&
|
||||
Object.keys(response.invalidProperties).length > 0;
|
||||
|
||||
if (response.error === true || hasInvalidProps) {
|
||||
return throwError(() => new ResponseArgsError(response));
|
||||
}
|
||||
}
|
||||
|
||||
return [response];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable, inject, resource, signal, computed } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { CustomerBonRedemptionFacade } from '../facades/customer-bon-redemption.facade';
|
||||
import { LoyaltyBonResponse } from '@generated/swagger/crm-api';
|
||||
import { ResponseArgsError } from '@isa/common/data-access';
|
||||
|
||||
/**
|
||||
* Resource for checking/validating Bon numbers.
|
||||
@@ -47,8 +48,24 @@ export class CustomerBonCheckResource {
|
||||
this.#logger.debug('Bon checked', () => ({
|
||||
bonNr,
|
||||
found: !!response?.result,
|
||||
hasInvalidProperties:
|
||||
!!response?.invalidProperties &&
|
||||
Object.keys(response.invalidProperties).length > 0,
|
||||
}));
|
||||
|
||||
// Check for invalidProperties even when error is false
|
||||
// Backend may return { error: false, invalidProperties: {...} } for validation issues
|
||||
if (
|
||||
response?.invalidProperties &&
|
||||
Object.keys(response.invalidProperties).length > 0
|
||||
) {
|
||||
this.#logger.warn('Bon check has invalid properties', () => ({
|
||||
bonNr,
|
||||
invalidProperties: response.invalidProperties,
|
||||
}));
|
||||
throw new ResponseArgsError(response);
|
||||
}
|
||||
|
||||
return response?.result;
|
||||
},
|
||||
defaultValue: undefined,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { CountryService as ApiCountryService } from '@generated/swagger/crm-api';
|
||||
import { Cache, CacheTimeToLive } from '@isa/common/decorators';
|
||||
import { Cache } from '@isa/common/decorators';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Country } from '../models';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
ResponseArgs,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CountryService {
|
||||
#apiCountryService = inject(ApiCountryService);
|
||||
#logger = logger(() => ({ service: 'CountryService' }));
|
||||
|
||||
@Cache()
|
||||
async getCountries(abortSignal?: AbortSignal): Promise<Country[]> {
|
||||
@@ -23,8 +24,12 @@ export class CountryService {
|
||||
|
||||
req$ = req$.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
return res.result as Country[];
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as Country[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to get countries', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
ResponseArgs,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { Cache, CacheTimeToLive } from '@isa/common/decorators';
|
||||
@@ -161,8 +160,15 @@ export class CrmSearchService {
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
return res?.result;
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res?.result;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error adding customer card', error, () => ({
|
||||
customerId: parsed.customerId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async lockCard(params: LockCardInput): Promise<boolean | undefined> {
|
||||
@@ -176,15 +182,15 @@ export class CrmSearchService {
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Lock card Failed', err);
|
||||
throw err;
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res?.result;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error locking customer card', error, () => ({
|
||||
cardCode: parsed.cardCode,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.result;
|
||||
}
|
||||
|
||||
async unlockCard(params: UnlockCardInput): Promise<boolean | undefined> {
|
||||
@@ -199,15 +205,16 @@ export class CrmSearchService {
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Unlock card Failed', err);
|
||||
throw err;
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res?.result;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error unlocking customer card', error, () => ({
|
||||
customerId: parsed.customerId,
|
||||
cardCode: parsed.cardCode,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.result;
|
||||
}
|
||||
|
||||
@Cache({ ttl: CacheTimeToLive.oneHour })
|
||||
@@ -227,11 +234,10 @@ export class CrmSearchService {
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully fetched current booking partner store');
|
||||
|
||||
return res?.result;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error fetching current booking partner store', error);
|
||||
return undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,8 +256,16 @@ export class CrmSearchService {
|
||||
},
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
const res = await firstValueFrom(req$);
|
||||
return res?.result;
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res?.result;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error adding booking', error, () => ({
|
||||
cardCode: parsed.cardCode,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,16 +292,12 @@ export class CrmSearchService {
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully checked Bon');
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Bon check failed', err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return res as ResponseArgs<LoyaltyBonResponse>;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error checking Bon', error);
|
||||
this.#logger.error('Error checking Bon', error, () => ({
|
||||
cardCode,
|
||||
bonNr,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -306,8 +316,16 @@ export class CrmSearchService {
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully redeemed Bon');
|
||||
return res?.result ?? false;
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully redeemed Bon');
|
||||
return res?.result ?? false;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error redeeming Bon', error, () => ({
|
||||
cardCode,
|
||||
bonNr,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,12 +114,7 @@ export class CrmFeatureCustomerBonRedemptionComponent {
|
||||
}
|
||||
// Handle API errors
|
||||
else if (error) {
|
||||
let errorMsg = 'Bon-Validierung fehlgeschlagen';
|
||||
if (error instanceof ResponseArgsError) {
|
||||
errorMsg = error.message || errorMsg;
|
||||
} else if (error instanceof Error) {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
const errorMsg = this.#extractErrorMessage(error);
|
||||
this.store.setError(errorMsg);
|
||||
}
|
||||
});
|
||||
@@ -224,4 +219,23 @@ export class CrmFeatureCustomerBonRedemptionComponent {
|
||||
this.store.reset();
|
||||
this.#bonCheckResource.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from various error types.
|
||||
* ResponseArgsError already formats invalidProperties into a readable message.
|
||||
*/
|
||||
#extractErrorMessage(error: unknown): string {
|
||||
const defaultMsg = 'Bon-Validierung fehlgeschlagen';
|
||||
const actualError = (error as { cause?: unknown })?.cause ?? error;
|
||||
|
||||
if (actualError instanceof ResponseArgsError) {
|
||||
return actualError.message || defaultMsg;
|
||||
}
|
||||
|
||||
if (actualError instanceof Error) {
|
||||
return actualError.message || defaultMsg;
|
||||
}
|
||||
|
||||
return defaultMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { ReceiptService } from '@generated/swagger/oms-api';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { Receipt } from '../models';
|
||||
|
||||
@Injectable()
|
||||
@@ -74,29 +77,31 @@ export class HandleCommandService {
|
||||
parsed,
|
||||
}));
|
||||
|
||||
let req$ = this.#receiptService.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: parsed, // Payload Default from old Implementation, eagerLoading: 1 and receiptType: (1 + 64 + 128) set as Schema default
|
||||
});
|
||||
let req$ = this.#receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: parsed, // Payload Default from old Implementation, eagerLoading: 1 and receiptType: (1 + 64 + 128) set as Schema default
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
// Mapping Logic from old implementation
|
||||
const mappedReceipts =
|
||||
res?.result?.map((r) => r.item3?.data).filter((f) => !!f) ?? [];
|
||||
|
||||
return mappedReceipts as Receipt[];
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch receipts by order item subset IDs',
|
||||
err,
|
||||
error,
|
||||
() => ({ ids: parsed.ids }),
|
||||
);
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Mapping Logic from old implementation
|
||||
const mappedReceipts =
|
||||
res?.result?.map((r) => r.item3?.data).filter((f) => !!f) ?? [];
|
||||
|
||||
return mappedReceipts as Receipt[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { LogisticianService as GeneratedLogisticianService } from '@generated/swagger/oms-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
|
||||
@@ -32,37 +35,38 @@ export class LogisticianService {
|
||||
async getLogistician2470(abortSignal?: AbortSignal): Promise<Logistician> {
|
||||
this.#logger.debug('Fetching logistician 2470');
|
||||
|
||||
let req$ = this.#logisticianService.LogisticianGetLogisticians({});
|
||||
let req$ = this.#logisticianService
|
||||
.LogisticianGetLogisticians({})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error || !res.result) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch logisticians', error);
|
||||
const logistician = res.result?.find(
|
||||
(l) => l.logisticianNumber === '2470',
|
||||
);
|
||||
|
||||
if (!logistician) {
|
||||
const notFoundError = new Error('Logistician 2470 not found');
|
||||
this.#logger.error('Logistician 2470 not found', notFoundError, () => ({
|
||||
availableLogisticians: res.result?.map((l) => l.logisticianNumber),
|
||||
}));
|
||||
throw notFoundError;
|
||||
}
|
||||
|
||||
this.#logger.debug('Logistician 2470 fetched', () => ({
|
||||
logisticianId: logistician.id,
|
||||
logisticianNumber: logistician.logisticianNumber,
|
||||
}));
|
||||
|
||||
return logistician;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch logistician 2470', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const logistician = res.result.find(
|
||||
(l) => l.logisticianNumber === '2470',
|
||||
);
|
||||
|
||||
if (!logistician) {
|
||||
const notFoundError = new Error('Logistician 2470 not found');
|
||||
this.#logger.error('Logistician 2470 not found', notFoundError, () => ({
|
||||
availableLogisticians: res.result?.map((l) => l.logisticianNumber),
|
||||
}));
|
||||
throw notFoundError;
|
||||
}
|
||||
|
||||
this.#logger.debug('Logistician 2470 fetched', () => ({
|
||||
logisticianId: logistician.id,
|
||||
logisticianNumber: logistician.logisticianNumber,
|
||||
}));
|
||||
|
||||
return logistician;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
DBHOrderItemListItemDTO,
|
||||
QueryTokenDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@@ -49,26 +52,26 @@ export class OpenRewardTasksService {
|
||||
orderBy: [],
|
||||
};
|
||||
|
||||
let req$ = this.#abholfachService.AbholfachWarenausgabe(payload);
|
||||
let req$ = this.#abholfachService
|
||||
.AbholfachWarenausgabe(payload)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
const tasks = res.result ?? [];
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.debug('Open reward tasks fetched', () => ({
|
||||
taskCount: tasks.length,
|
||||
}));
|
||||
|
||||
return tasks;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch open reward tasks', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const tasks = res.result ?? [];
|
||||
|
||||
this.#logger.debug('Open reward tasks fetched', () => ({
|
||||
taskCount: tasks.length,
|
||||
}));
|
||||
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
LogisticianService,
|
||||
LogisticianDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { DisplayOrder } from '../models';
|
||||
@@ -36,19 +39,23 @@ export class OrderCreationService {
|
||||
throw new Error(`Invalid checkoutId: ${checkoutId}`);
|
||||
}
|
||||
|
||||
const req$ = this.#orderCheckoutService.OrderCheckoutCreateOrderPOST({
|
||||
checkoutId,
|
||||
});
|
||||
const req$ = this.#orderCheckoutService
|
||||
.OrderCheckoutCreateOrderPOST({
|
||||
checkoutId,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to create orders', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as DisplayOrder[];
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to create orders from checkout',
|
||||
error,
|
||||
() => ({ checkoutId }),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as DisplayOrder[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,25 +70,29 @@ export class OrderCreationService {
|
||||
logisticianNumber = '2470',
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<LogisticianDTO> {
|
||||
let req$ = this.#logisticianService.LogisticianGetLogisticians({});
|
||||
let req$ = this.#logisticianService
|
||||
.LogisticianGetLogisticians({})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to get logistician', error);
|
||||
const logistician = res.result?.find(
|
||||
(l) => l.logisticianNumber === logisticianNumber,
|
||||
);
|
||||
|
||||
if (!logistician) {
|
||||
throw new Error(`Logistician ${logisticianNumber} not found`);
|
||||
}
|
||||
|
||||
return logistician;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to get logistician', error, () => ({
|
||||
logisticianNumber,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
const logistician = res.result?.find(
|
||||
(l) => l.logisticianNumber === logisticianNumber,
|
||||
);
|
||||
|
||||
if (!logistician) {
|
||||
throw new Error(`Logistician ${logisticianNumber} not found`);
|
||||
}
|
||||
|
||||
return logistician;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { OrderService } from '@generated/swagger/oms-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
@@ -25,25 +28,28 @@ export class OrderRewardCollectService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const req$ = this.#orderService.OrderLoyaltyCollect({
|
||||
orderId: params.orderId,
|
||||
orderItemId: params.orderItemId,
|
||||
orderItemSubsetId: params.orderItemSubsetId,
|
||||
data: {
|
||||
collectType: params.collectType,
|
||||
quantity: params.quantity,
|
||||
},
|
||||
});
|
||||
const req$ = this.#orderService
|
||||
.OrderLoyaltyCollect({
|
||||
orderId: params.orderId,
|
||||
orderItemId: params.orderItemId,
|
||||
orderItemSubsetId: params.orderItemSubsetId,
|
||||
data: {
|
||||
collectType: params.collectType,
|
||||
quantity: params.quantity,
|
||||
},
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to collect reward item', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as DBHOrderItemListItem[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to collect order reward', error, () => ({
|
||||
orderId: params.orderId,
|
||||
orderItemSubsetId: params.orderItemSubsetId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as DBHOrderItemListItem[];
|
||||
}
|
||||
|
||||
async fetchOrderItemSubset(
|
||||
@@ -57,22 +63,22 @@ export class OrderRewardCollectService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let req$ = this.#orderService.OrderGetOrderItemSubset(
|
||||
params.orderItemSubsetId,
|
||||
);
|
||||
let req$ = this.#orderService
|
||||
.OrderGetOrderItemSubset(params.orderItemSubsetId)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch order item subset', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result as DisplayOrderItemSubset;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch order item subset', error, () => ({
|
||||
orderItemSubsetId: params.orderItemSubsetId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as DisplayOrderItemSubset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
OrderDTO,
|
||||
DisplayOrderDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@@ -20,21 +23,23 @@ export class OrdersService {
|
||||
orderId: number,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<OrderDTO | null> {
|
||||
let req$ = this.#orderService.OrderGetOrder(orderId);
|
||||
let req$ = this.#orderService
|
||||
.OrderGetOrder(orderId)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch order', { orderId, error });
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result ?? null;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to get order', error, () => ({
|
||||
orderId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,21 +71,23 @@ export class OrdersService {
|
||||
orderId: number,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<DisplayOrderDTO | null> {
|
||||
let req$ = this.#orderService.OrderGetDisplayOrder(orderId);
|
||||
let req$ = this.#orderService
|
||||
.OrderGetDisplayOrder(orderId)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch display order', { orderId, error });
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
return res.result ?? null;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to get display order', error, () => ({
|
||||
orderId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
returnReceiptValuesMapping,
|
||||
} from '../helpers/return-process';
|
||||
import { isReturnProcessTypeGuard } from '../guards';
|
||||
import { takeUntilAborted } from '@isa/common/data-access';
|
||||
|
||||
/**
|
||||
* Service for determining if a return process can proceed based on
|
||||
@@ -36,20 +35,14 @@ export class ReturnCanReturnService {
|
||||
* @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,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<CanReturn | undefined>;
|
||||
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,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<CanReturn>;
|
||||
async canReturn(returnValues: ReturnReceiptValues): Promise<CanReturn>;
|
||||
|
||||
/**
|
||||
* Determines if a return can proceed, accepting either a ReturnProcess or ReturnReceiptValues.
|
||||
@@ -60,7 +53,6 @@ export class ReturnCanReturnService {
|
||||
*/
|
||||
async canReturn(
|
||||
input: ReturnProcess | ReturnReceiptValues,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<CanReturn | undefined> {
|
||||
let data: ReturnReceiptValues | undefined = undefined;
|
||||
|
||||
@@ -74,14 +66,10 @@ export class ReturnCanReturnService {
|
||||
return undefined; // Prozess soll weitergehen, daher kein Error
|
||||
}
|
||||
|
||||
let req$ = this.#receiptService.ReceiptCanReturn(
|
||||
const req$ = this.#receiptService.ReceiptCanReturn(
|
||||
data as ReturnReceiptValuesDTO,
|
||||
);
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
req$.pipe(
|
||||
|
||||
@@ -32,19 +32,19 @@ export class ReturnDetailsService {
|
||||
* @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.
|
||||
* @param abortSignal - Optional AbortSignal to cancel the request.
|
||||
* @returns A promise resolving to the result of the canReturn check, containing
|
||||
* eligibility status and any relevant constraints or messages.
|
||||
* @throws Will throw an error if the return check fails or is aborted.
|
||||
* @throws Will throw an error if the return check fails.
|
||||
*/
|
||||
async canReturn(
|
||||
{
|
||||
receiptItemId,
|
||||
quantity,
|
||||
category,
|
||||
}: { receiptItemId: number; quantity: number; category: ProductCategory },
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<CanReturn> {
|
||||
async canReturn({
|
||||
receiptItemId,
|
||||
quantity,
|
||||
category,
|
||||
}: {
|
||||
receiptItemId: number;
|
||||
quantity: number;
|
||||
category: ProductCategory;
|
||||
}): Promise<CanReturn> {
|
||||
const returnReceiptValues: ReturnReceiptValues = {
|
||||
quantity,
|
||||
receiptItem: {
|
||||
@@ -53,10 +53,7 @@ export class ReturnDetailsService {
|
||||
category,
|
||||
};
|
||||
|
||||
return this.#returnCanReturnService.canReturn(
|
||||
returnReceiptValues,
|
||||
abortSignal,
|
||||
);
|
||||
return this.#returnCanReturnService.canReturn(returnReceiptValues);
|
||||
}
|
||||
/**
|
||||
* Gets all available product categories that have defined question sets.
|
||||
|
||||
@@ -176,7 +176,7 @@ export const ReturnDetailsStore = signalStore(
|
||||
category,
|
||||
};
|
||||
},
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
loader: async ({ params }) => {
|
||||
if (params === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -186,10 +186,7 @@ export const ReturnDetailsStore = signalStore(
|
||||
return store.canReturn()[key];
|
||||
}
|
||||
|
||||
const res = await store._returnDetailsService.canReturn(
|
||||
params,
|
||||
abortSignal,
|
||||
);
|
||||
const res = await store._returnDetailsService.canReturn(params);
|
||||
patchState(store, {
|
||||
canReturn: { ...store.canReturn(), [key]: res },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { StockService, BranchDTO } from '@generated/swagger/inventory-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@@ -27,41 +30,42 @@ export class BranchService {
|
||||
* @throws {Error} If branch retrieval fails
|
||||
*/
|
||||
async getDefaultBranch(abortSignal?: AbortSignal): Promise<BranchDTO> {
|
||||
let req$ = this.#stockService.StockCurrentBranch();
|
||||
let req$ = this.#stockService
|
||||
.StockCurrentBranch()
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
const branch = res.result;
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
if (!branch) {
|
||||
const error = new Error('No branch data returned');
|
||||
this.#logger.error('Failed to get default branch', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
address: branch.address,
|
||||
branchType: branch.branchType,
|
||||
branchNumber: branch.branchNumber,
|
||||
changed: branch.changed,
|
||||
created: branch.created,
|
||||
isDefault: branch.isDefault,
|
||||
isOnline: branch.isOnline,
|
||||
key: branch.key,
|
||||
label: branch.label,
|
||||
pId: branch.pId,
|
||||
shortName: branch.shortName,
|
||||
status: branch.status,
|
||||
version: branch.version,
|
||||
};
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to get default branch', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const branch = res.result;
|
||||
|
||||
if (!branch) {
|
||||
const error = new Error('No branch data returned');
|
||||
this.#logger.error('Failed to get default branch', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
address: branch.address,
|
||||
branchType: branch.branchType,
|
||||
branchNumber: branch.branchNumber,
|
||||
changed: branch.changed,
|
||||
created: branch.created,
|
||||
isDefault: branch.isDefault,
|
||||
isOnline: branch.isOnline,
|
||||
key: branch.key,
|
||||
label: branch.label,
|
||||
pId: branch.pId,
|
||||
shortName: branch.shortName,
|
||||
status: branch.status,
|
||||
version: branch.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import { KeyValueStringAndString } from '../models';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { RemissionStockService } from './remission-stock.service';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { InFlight, Cache, CacheTimeToLive } from '@isa/common/decorators';
|
||||
|
||||
/**
|
||||
@@ -65,26 +68,29 @@ export class RemissionProductGroupService {
|
||||
stockId: assignedStock.id,
|
||||
}));
|
||||
|
||||
let req$ = this.#remiService.RemiProductgroups({
|
||||
stockId: assignedStock.id,
|
||||
});
|
||||
let req$ = this.#remiService
|
||||
.RemiProductgroups({
|
||||
stockId: assignedStock.id,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error || !res.result) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch product groups', error);
|
||||
this.#logger.debug('Successfully fetched product groups', () => ({
|
||||
groupCount: res.result?.length || 0,
|
||||
}));
|
||||
|
||||
return res.result as KeyValueStringAndString[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch product groups', error, () => ({
|
||||
stockId: assignedStock.id,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.debug('Successfully fetched product groups', () => ({
|
||||
groupCount: res.result?.length || 0,
|
||||
}));
|
||||
|
||||
return res.result as KeyValueStringAndString[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ReturnService } from '@generated/swagger/inventory-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { KeyValueStringAndString } from '../models';
|
||||
import { logger } from '@isa/core/logging';
|
||||
@@ -74,29 +77,30 @@ export class RemissionReasonService {
|
||||
stockId: assignedStock?.id,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnGetReturnReasons({
|
||||
stockId: assignedStock?.id,
|
||||
});
|
||||
let req$ = this.#returnService
|
||||
.ReturnGetReturnReasons({
|
||||
stockId: assignedStock?.id,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch return reasons',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
this.#logger.debug('Successfully fetched return reasons', () => ({
|
||||
reasonCount: res.result?.length || 0,
|
||||
}));
|
||||
|
||||
return res.result as KeyValueStringAndString[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch return reasons', error, () => ({
|
||||
stockId: assignedStock?.id,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.debug('Successfully fetched return reasons', () => ({
|
||||
reasonCount: res.result?.length || 0,
|
||||
}));
|
||||
|
||||
return res.result as KeyValueStringAndString[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,15 +540,15 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle abort signal', async () => {
|
||||
it('should call API with correct parameters', async () => {
|
||||
mockReturnService.ReturnCreateAndAssignPackage.mockReturnValue(
|
||||
of({ result: mockReceipt, error: null }),
|
||||
);
|
||||
const abortController = new AbortController();
|
||||
await service.assignPackage(
|
||||
{ returnId: 123, receiptId: 456, packageNumber: 'PKG-789' },
|
||||
abortController.signal,
|
||||
);
|
||||
await service.assignPackage({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
expect(mockReturnService.ReturnCreateAndAssignPackage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1052,24 +1052,20 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle abort signal', async () => {
|
||||
it('should call API with correct parameters', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnAddReturnItem.mockReturnValue(
|
||||
of({ result: mockTuple, error: null }),
|
||||
);
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Act
|
||||
await service.addReturnItem(
|
||||
{
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnItemId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
await service.addReturnItem({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnItemId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockReturnService.ReturnAddReturnItem).toHaveBeenCalled();
|
||||
@@ -1163,24 +1159,20 @@ describe('RemissionReturnReceiptService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle abort signal', async () => {
|
||||
it('should call API with correct parameters', async () => {
|
||||
// Arrange
|
||||
mockReturnService.ReturnAddReturnSuggestion.mockReturnValue(
|
||||
of({ result: mockTuple, error: null }),
|
||||
);
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Act
|
||||
await service.addReturnSuggestionItem(
|
||||
{
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
await service.addReturnSuggestionItem({
|
||||
returnId: 1,
|
||||
receiptId: 2,
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockReturnService.ReturnAddReturnSuggestion).toHaveBeenCalled();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ReturnService } from '@generated/swagger/inventory-api';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
ResponseArgs,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
@@ -102,86 +102,21 @@ export class RemissionReturnReceiptService {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch completed returns',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
const returns = (res?.result as Return[]) || [];
|
||||
this.#logger.debug('Successfully fetched completed returns', () => ({
|
||||
returnCount: returns.length,
|
||||
}));
|
||||
|
||||
return returns;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch completed returns', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const returns = (res?.result as Return[]) || [];
|
||||
this.#logger.debug('Successfully fetched completed returns', () => ({
|
||||
returnCount: returns.length,
|
||||
}));
|
||||
|
||||
return returns;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Fetches a specific remission return receipt by receipt and return IDs.
|
||||
// * Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
// *
|
||||
// * @async
|
||||
// * @param {FetchRemissionReturnParams} params - The receipt and return identifiers
|
||||
// * @param {FetchRemissionReturnParams} params.receiptId - ID of the receipt to fetch
|
||||
// * @param {FetchRemissionReturnParams} params.returnId - ID of the return containing the receipt
|
||||
// * @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
// * @returns {Promise<Receipt | undefined>} The receipt object if found, undefined otherwise
|
||||
// * @throws {ResponseArgsError} When the API request fails
|
||||
// * @throws {z.ZodError} When parameter validation fails
|
||||
// *
|
||||
// * @example
|
||||
// * const receipt = await service.fetchRemissionReturnReceipt({
|
||||
// * receiptId: '123',
|
||||
// * returnId: '456'
|
||||
// * });
|
||||
// */
|
||||
// async fetchRemissionReturnReceipt(
|
||||
// params: FetchRemissionReturnParams,
|
||||
// abortSignal?: AbortSignal,
|
||||
// ): Promise<Receipt | undefined> {
|
||||
// this.#logger.debug('Fetching remission return receipt', () => ({ params }));
|
||||
|
||||
// const { receiptId, returnId } =
|
||||
// FetchRemissionReturnReceiptSchema.parse(params);
|
||||
|
||||
// this.#logger.info('Fetching return receipt from API', () => ({
|
||||
// receiptId,
|
||||
// returnId,
|
||||
// }));
|
||||
|
||||
// let req$ = this.#returnService.ReturnGetReturnReceipt({
|
||||
// receiptId,
|
||||
// returnId,
|
||||
// eagerLoading: 2,
|
||||
// });
|
||||
|
||||
// if (abortSignal) {
|
||||
// this.#logger.debug('Request configured with abort signal');
|
||||
// req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
// }
|
||||
|
||||
// const res = await firstValueFrom(req$);
|
||||
|
||||
// if (res?.error) {
|
||||
// this.#logger.error(
|
||||
// 'Failed to fetch return receipt',
|
||||
// new Error(res.message || 'Unknown error'),
|
||||
// );
|
||||
// throw new ResponseArgsError(res);
|
||||
// }
|
||||
|
||||
// const receipt = res?.result as Receipt | undefined;
|
||||
// this.#logger.debug('Successfully fetched return receipt', () => ({
|
||||
// found: !!receipt,
|
||||
// }));
|
||||
|
||||
// return receipt;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Fetches a remission return by its ID.
|
||||
* Validates parameters using FetchReturnSchema before making the request.
|
||||
@@ -218,22 +153,19 @@ export class RemissionReturnReceiptService {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
const returnData = res?.result as Return | undefined;
|
||||
this.#logger.debug('Successfully fetched return', () => ({
|
||||
found: !!returnData,
|
||||
}));
|
||||
|
||||
return returnData;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch return', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const returnData = res?.result as Return | undefined;
|
||||
this.#logger.debug('Successfully fetched return', () => ({
|
||||
found: !!returnData,
|
||||
}));
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,34 +211,26 @@ export class RemissionReturnReceiptService {
|
||||
returnGroup,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnCreateReturn({
|
||||
const req$ = this.#returnService.ReturnCreateReturn({
|
||||
data: {
|
||||
supplier: { id: firstSupplier.id },
|
||||
returnGroup,
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
const returnResponse = res as ResponseArgs<Return> | undefined;
|
||||
this.#logger.debug('Successfully created return', () => ({
|
||||
found: !!returnResponse,
|
||||
}));
|
||||
|
||||
return returnResponse;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to create return', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to create return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const returnResponse = res as ResponseArgs<Return> | undefined;
|
||||
this.#logger.debug('Successfully created return', () => ({
|
||||
found: !!returnResponse,
|
||||
}));
|
||||
|
||||
return returnResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,7 +269,7 @@ export class RemissionReturnReceiptService {
|
||||
receiptNumber,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnCreateReceipt({
|
||||
const req$ = this.#returnService.ReturnCreateReceipt({
|
||||
returnId,
|
||||
data: {
|
||||
receiptNumber,
|
||||
@@ -359,27 +283,19 @@ export class RemissionReturnReceiptService {
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
const receiptResponse = res as ResponseArgs<Receipt> | undefined;
|
||||
this.#logger.debug('Successfully created return receipt', () => ({
|
||||
found: !!receiptResponse,
|
||||
}));
|
||||
|
||||
return receiptResponse;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to create return receipt', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to create return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receiptResponse = res as ResponseArgs<Receipt> | undefined;
|
||||
this.#logger.debug('Successfully created return receipt', () => ({
|
||||
found: !!receiptResponse,
|
||||
}));
|
||||
|
||||
return receiptResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,7 +318,6 @@ export class RemissionReturnReceiptService {
|
||||
*/
|
||||
async assignPackage(
|
||||
params: AssignPackage,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ResponseArgs<Receipt> | undefined> {
|
||||
this.#logger.debug('Assign package to return receipt', () => ({ params }));
|
||||
|
||||
@@ -414,7 +329,7 @@ export class RemissionReturnReceiptService {
|
||||
packageNumber,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnCreateAndAssignPackage({
|
||||
const req$ = this.#returnService.ReturnCreateAndAssignPackage({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {
|
||||
@@ -422,29 +337,21 @@ export class RemissionReturnReceiptService {
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
const receiptWithAssignedPackageResponse = res as
|
||||
| ResponseArgs<Receipt>
|
||||
| undefined;
|
||||
|
||||
this.#logger.debug('Successfully assigned package', () => ({
|
||||
found: !!receiptWithAssignedPackageResponse,
|
||||
}));
|
||||
return receiptWithAssignedPackageResponse;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to assign package', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to assign package',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receiptWithAssignedPackageResponse = res as
|
||||
| ResponseArgs<Receipt>
|
||||
| undefined;
|
||||
|
||||
this.#logger.debug('Successfully assigned package', () => ({
|
||||
found: !!receiptWithAssignedPackageResponse,
|
||||
}));
|
||||
return receiptWithAssignedPackageResponse;
|
||||
}
|
||||
|
||||
async removeReturnItemFromReturnReceipt(params: {
|
||||
@@ -452,16 +359,15 @@ export class RemissionReturnReceiptService {
|
||||
receiptId: number;
|
||||
receiptItemId: number;
|
||||
}) {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnRemoveReturnItem(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to remove item from return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
try {
|
||||
await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnRemoveReturnItem(params)
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remove item from return receipt', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,16 +386,17 @@ export class RemissionReturnReceiptService {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}): Promise<void> {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnCancelReturnReceipt(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to cancel return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
try {
|
||||
await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnCancelReturnReceipt(params)
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to cancel return receipt', error, () => ({
|
||||
params,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,34 +409,36 @@ export class RemissionReturnReceiptService {
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*/
|
||||
async cancelReturn(params: { returnId: number }): Promise<void> {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnCancelReturn(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to cancel return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
try {
|
||||
await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnCancelReturn(params)
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to cancel return', error, () => ({
|
||||
params,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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'),
|
||||
try {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnDeleteReturnItem(params)
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
return res?.result as ReturnItem;
|
||||
return res?.result as ReturnItem;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to delete return item', error, () => ({
|
||||
params,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateReturnItemImpediment(params: UpdateItemImpediment) {
|
||||
@@ -537,24 +446,27 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
const { itemId, comment } = UpdateItemImpedimentSchema.parse(params);
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnReturnItemImpediment({
|
||||
itemId,
|
||||
data: {
|
||||
comment,
|
||||
},
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnReturnItemImpediment({
|
||||
itemId,
|
||||
data: {
|
||||
comment,
|
||||
},
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
return res?.result as ReturnItem;
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to update return item impediment',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
error,
|
||||
() => ({ itemId, comment }),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.result as ReturnItem;
|
||||
}
|
||||
|
||||
async updateReturnSuggestionImpediment(params: UpdateItemImpediment) {
|
||||
@@ -562,22 +474,26 @@ export class RemissionReturnReceiptService {
|
||||
params,
|
||||
}));
|
||||
const { itemId, comment } = UpdateItemImpedimentSchema.parse(params);
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnReturnSuggestionImpediment({
|
||||
itemId,
|
||||
data: {
|
||||
comment,
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (res?.error) {
|
||||
try {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnReturnSuggestionImpediment({
|
||||
itemId,
|
||||
data: {
|
||||
comment,
|
||||
},
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
return res?.result as ReturnSuggestion;
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to update return suggestion impediment',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
error,
|
||||
() => ({ itemId, comment }),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
throw error;
|
||||
}
|
||||
return res?.result as ReturnSuggestion;
|
||||
}
|
||||
|
||||
async completeReturnReceipt({
|
||||
@@ -588,23 +504,25 @@ export class RemissionReturnReceiptService {
|
||||
receiptId: number;
|
||||
}): Promise<Receipt> {
|
||||
this.#logger.debug('Completing return receipt', () => ({ returnId }));
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReceipt({
|
||||
try {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnFinalizeReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {},
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
|
||||
return res?.result as Receipt;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to complete return receipt', error, () => ({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.result as Receipt;
|
||||
}
|
||||
|
||||
async completeReturn(params: { returnId: number }): Promise<Return> {
|
||||
@@ -612,23 +530,24 @@ export class RemissionReturnReceiptService {
|
||||
returnId: params.returnId,
|
||||
}));
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReturn(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
try {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnFinalizeReturn(params)
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
|
||||
this.#logger.info('Successfully completed return', () => ({
|
||||
returnId: params.returnId,
|
||||
}));
|
||||
|
||||
return res?.result as Return;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to complete return', error, () => ({
|
||||
returnId: params.returnId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.info('Successfully completed return', () => ({
|
||||
returnId: params.returnId,
|
||||
}));
|
||||
|
||||
return res?.result as Return;
|
||||
}
|
||||
|
||||
async completeReturnGroup(params: { returnGroup: string }) {
|
||||
@@ -636,23 +555,24 @@ export class RemissionReturnReceiptService {
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReturnGroup(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return group',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
try {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService
|
||||
.ReturnFinalizeReturnGroup(params)
|
||||
.pipe(catchResponseArgsErrorPipe()),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
|
||||
this.#logger.info('Successfully completed return group', () => ({
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
return res?.result as Return[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to complete return group', error, () => ({
|
||||
returnGroup: params.returnGroup,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.info('Successfully completed return group', () => ({
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
return res?.result as Return[];
|
||||
}
|
||||
|
||||
async completeReturnReceiptAndReturn(params: {
|
||||
@@ -703,7 +623,6 @@ export class RemissionReturnReceiptService {
|
||||
*/
|
||||
async addReturnItem(
|
||||
params: AddReturnItem,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ReceiptReturnTuple | undefined> {
|
||||
this.#logger.debug('Adding return item', () => ({ params }));
|
||||
|
||||
@@ -718,7 +637,7 @@ export class RemissionReturnReceiptService {
|
||||
inStock,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnAddReturnItem({
|
||||
const req$ = this.#returnService.ReturnAddReturnItem({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {
|
||||
@@ -728,27 +647,23 @@ export class RemissionReturnReceiptService {
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
const updatedReturn = res?.result as ReceiptReturnTuple | undefined;
|
||||
this.#logger.debug('Successfully added return item', () => ({
|
||||
found: !!updatedReturn,
|
||||
}));
|
||||
|
||||
return updatedReturn;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to add return item', error, () => ({
|
||||
returnId,
|
||||
receiptId,
|
||||
returnItemId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to add return item',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const updatedReturn = res?.result as ReceiptReturnTuple | undefined;
|
||||
this.#logger.debug('Successfully added return item', () => ({
|
||||
found: !!updatedReturn,
|
||||
}));
|
||||
|
||||
return updatedReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -775,7 +690,6 @@ export class RemissionReturnReceiptService {
|
||||
*/
|
||||
async addReturnSuggestionItem(
|
||||
params: AddReturnSuggestionItem,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ReceiptReturnSuggestionTuple | undefined> {
|
||||
this.#logger.debug('Adding return suggestion item', () => ({ params }));
|
||||
|
||||
@@ -799,7 +713,7 @@ export class RemissionReturnReceiptService {
|
||||
remainingQuantity,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnAddReturnSuggestion({
|
||||
const req$ = this.#returnService.ReturnAddReturnSuggestion({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {
|
||||
@@ -811,29 +725,25 @@ export class RemissionReturnReceiptService {
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
const updatedReturnSuggestion = res?.result as
|
||||
| ReceiptReturnSuggestionTuple
|
||||
| undefined;
|
||||
this.#logger.debug('Successfully added return suggestion item', () => ({
|
||||
found: !!updatedReturnSuggestion,
|
||||
}));
|
||||
|
||||
return updatedReturnSuggestion;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to add return suggestion item', error, () => ({
|
||||
returnId,
|
||||
receiptId,
|
||||
returnSuggestionId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to add return suggestion item',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const updatedReturnSuggestion = res?.result as
|
||||
| ReceiptReturnSuggestionTuple
|
||||
| undefined;
|
||||
this.#logger.debug('Successfully added return suggestion item', () => ({
|
||||
found: !!updatedReturnSuggestion,
|
||||
}));
|
||||
|
||||
return updatedReturnSuggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -853,22 +763,23 @@ export class RemissionReturnReceiptService {
|
||||
* receiptId: 456,
|
||||
* });
|
||||
*/
|
||||
async createRemission({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
}: {
|
||||
returnGroup: string | undefined;
|
||||
receiptNumber: string | undefined;
|
||||
}): Promise<CreateRemission | undefined> {
|
||||
async createRemission(
|
||||
{
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
}: {
|
||||
returnGroup: string | undefined;
|
||||
receiptNumber: string | undefined;
|
||||
},
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<CreateRemission | undefined> {
|
||||
this.#logger.debug('Create remission', () => ({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
}));
|
||||
|
||||
const createdReturn: ResponseArgs<Return> | undefined =
|
||||
await this.createReturn({
|
||||
returnGroup,
|
||||
});
|
||||
await this.createReturn({ returnGroup }, abortSignal);
|
||||
|
||||
if (!createdReturn || !createdReturn.result) {
|
||||
this.#logger.error('Failed to create return for remission');
|
||||
@@ -876,10 +787,13 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
|
||||
const createdReceipt: ResponseArgs<Receipt> | undefined =
|
||||
await this.createReceipt({
|
||||
returnId: createdReturn.result.id,
|
||||
receiptNumber,
|
||||
});
|
||||
await this.createReceipt(
|
||||
{
|
||||
returnId: createdReturn.result.id,
|
||||
receiptNumber,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
if (!createdReceipt || !createdReceipt.result) {
|
||||
this.#logger.error('Failed to create return receipt');
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
BatchResponseArgs,
|
||||
catchResponseArgsErrorPipe,
|
||||
ListResponseArgs,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
@@ -122,16 +122,18 @@ export class RemissionSearchService {
|
||||
},
|
||||
});
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to fetch required capacity', error);
|
||||
this.#logger.debug('Successfully fetched required capacity');
|
||||
return (res?.result ?? []) as ValueTupleOfStringAndInteger[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch required capacity', error, () => ({
|
||||
stockId: parsed.stockId,
|
||||
supplierId: parsed.supplierId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.debug('Successfully fetched required capacity');
|
||||
return (res?.result ?? []) as ValueTupleOfStringAndInteger[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,14 +403,18 @@ export class RemissionSearchService {
|
||||
req = req.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req);
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to check item addition', error);
|
||||
try {
|
||||
const res = await firstValueFrom(req.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
return res as BatchResponseArgs<ReturnItem>;
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to check if items can be added to remission list',
|
||||
error,
|
||||
() => ({ stockId: stock.id, itemCount: items.length }),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res as BatchResponseArgs<ReturnItem>;
|
||||
}
|
||||
|
||||
async addToList(
|
||||
@@ -437,14 +443,17 @@ export class RemissionSearchService {
|
||||
})),
|
||||
});
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$.pipe(catchResponseArgsErrorPipe()));
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to add item to remission list', error);
|
||||
return res.successful?.map((r) => r.value) as ReturnItem[];
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to add items to remission list',
|
||||
error,
|
||||
() => ({ stockId: stock.id, itemCount: items.length }),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.successful?.map((r) => r.value) as ReturnItem[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,13 +87,13 @@ describe('RemissionStockService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns no result', async () => {
|
||||
it('should throw Error when API returns no result', async () => {
|
||||
mockStockService.StockCurrentStock.mockReturnValue(
|
||||
of({ error: false, result: undefined }),
|
||||
);
|
||||
|
||||
await expect(service.fetchAssignedStock()).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
'Assigned stock has no ID',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -196,16 +196,17 @@ describe('RemissionStockService', () => {
|
||||
expect(mockStockService.StockInStock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw ResponseArgsError when API returns no result', async () => {
|
||||
it('should return empty array when API returns no result', async () => {
|
||||
// Arrange
|
||||
mockStockService.StockInStock.mockReturnValue(
|
||||
of({ error: false, result: undefined }),
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.fetchStockInfos(validParams)).rejects.toThrow(
|
||||
ResponseArgsError,
|
||||
);
|
||||
// Act
|
||||
const result = await service.fetchStockInfos(validParams);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(undefined);
|
||||
expect(mockStockService.StockInStock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { StockService } from '@generated/swagger/inventory-api';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Stock, StockInfo } from '../models';
|
||||
import { FetchStockInStock, FetchStockInStockSchema } from '../schemas';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { InFlight, Cache, CacheTimeToLive } from '@isa/common/decorators';
|
||||
|
||||
@@ -56,73 +59,74 @@ export class RemissionStockService {
|
||||
@InFlight()
|
||||
async fetchAssignedStock(abortSignal?: AbortSignal): Promise<Stock> {
|
||||
this.#logger.info('Fetching assigned stock from API');
|
||||
let req$ = this.#stockService.StockCurrentStock();
|
||||
let req$ = this.#stockService
|
||||
.StockCurrentStock()
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
const result = res.result;
|
||||
if (result?.id === undefined) {
|
||||
const error = new Error('Assigned stock has no ID');
|
||||
this.#logger.error('Invalid stock response', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (res.error || !res.result) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch assigned stock',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
this.#logger.debug('Successfully fetched assigned stock', () => ({
|
||||
stockId: result.id,
|
||||
}));
|
||||
|
||||
const result = res.result;
|
||||
if (result.id === undefined) {
|
||||
const error = new Error('Assigned stock has no ID');
|
||||
this.#logger.error('Invalid stock response', error);
|
||||
// TypeScript cannot narrow StockDTO to Stock based on the id check above,
|
||||
// so we use a minimal type assertion after runtime validation
|
||||
return result as Stock;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch assigned stock', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.debug('Successfully fetched assigned stock', () => ({
|
||||
stockId: result.id,
|
||||
}));
|
||||
|
||||
// TypeScript cannot narrow StockDTO to Stock based on the id check above,
|
||||
// so we use a minimal type assertion after runtime validation
|
||||
return result as Stock;
|
||||
}
|
||||
|
||||
async fetchStock(
|
||||
branchId: number,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Stock | undefined> {
|
||||
let req$ = this.#stockService.StockGetStocks();
|
||||
let req$ = this.#stockService
|
||||
.StockGetStocks()
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error || !res.result) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch stocks',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
const stock = res.result?.find((s) => s.branch?.id === branchId);
|
||||
if (!stock) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (stock.id === undefined) {
|
||||
this.#logger.warn('Found stock without ID for branch', () => ({
|
||||
branchId,
|
||||
}));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TypeScript cannot narrow StockDTO to Stock based on the id check above,
|
||||
// so we use a minimal type assertion after runtime validation
|
||||
return stock as Stock;
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch stock', error, () => ({
|
||||
branchId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
const stock = res.result.find((s) => s.branch?.id === branchId);
|
||||
if (!stock) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (stock.id === undefined) {
|
||||
this.#logger.warn('Found stock without ID for branch', () => ({ branchId }));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TypeScript cannot narrow StockDTO to Stock based on the id check above,
|
||||
// so we use a minimal type assertion after runtime validation
|
||||
return stock as Stock;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,30 +183,31 @@ export class RemissionStockService {
|
||||
itemCount: parsed.itemIds.length,
|
||||
}));
|
||||
|
||||
let req$ = this.#stockService.StockInStock({
|
||||
stockId: assignedStockId,
|
||||
articleIds: parsed.itemIds,
|
||||
});
|
||||
let req$ = this.#stockService
|
||||
.StockInStock({
|
||||
stockId: assignedStockId,
|
||||
articleIds: parsed.itemIds,
|
||||
})
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
this.#logger.debug('Request configured with abort signal');
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error || !res.result) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch stock info',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
this.#logger.debug('Successfully fetched stock info', () => ({
|
||||
itemCount: res.result?.length || 0,
|
||||
}));
|
||||
|
||||
return res.result as StockInfo[];
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to fetch stock info', error, () => ({
|
||||
stockId: assignedStockId,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#logger.debug('Successfully fetched stock info', () => ({
|
||||
itemCount: res.result?.length || 0,
|
||||
}));
|
||||
|
||||
return res.result as StockInfo[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -63,6 +63,18 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
uiButton
|
||||
color="tertiary"
|
||||
size="large"
|
||||
(click)="abortRemission()"
|
||||
class="fixed right-[15rem] bottom-6"
|
||||
>
|
||||
Warenbegleitschein abbrechen
|
||||
</button>
|
||||
|
||||
@if (!returnLoading() && !returnData()?.completed) {
|
||||
<lib-remission-return-receipt-complete
|
||||
[returnId]="returnId()"
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getPackageNumbersFromReturn,
|
||||
getReceiptItemsFromReturn,
|
||||
getReceiptNumberFromReturn,
|
||||
RemissionStore,
|
||||
} from '@isa/remission/data-access';
|
||||
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import { EMPTY_WBS_DESCRIPTION, EMPTY_WBS_TITLE } from './constants';
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
RemissionReturnReceiptActionsComponent,
|
||||
RemissionReturnReceiptCompleteComponent,
|
||||
} from '@isa/remission/shared/return-receipt-actions';
|
||||
import { Router } from '@angular/router';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-remission-return-receipt-details',
|
||||
@@ -53,6 +56,15 @@ export class RemissionReturnReceiptDetailsComponent {
|
||||
/** Angular Location service for navigation */
|
||||
location = inject(Location);
|
||||
|
||||
/** Remission store for managing remission state */
|
||||
#store = inject(RemissionStore);
|
||||
|
||||
/** Angular Router for navigation */
|
||||
#router = inject(Router);
|
||||
|
||||
/** Injects the current activated tab ID as a signal. */
|
||||
#tabId = injectTabId();
|
||||
|
||||
/**
|
||||
* Required input for the return ID.
|
||||
* Automatically coerced to a number from string input.
|
||||
@@ -111,4 +123,9 @@ export class RemissionReturnReceiptDetailsComponent {
|
||||
const returnData = this.returnData();
|
||||
return getPackageNumbersFromReturn(returnData!) !== '';
|
||||
});
|
||||
|
||||
async abortRemission() {
|
||||
this.#store.clearState();
|
||||
await this.#router.navigate(['/', this.#tabId(), 'remission']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="open()"
|
||||
[cdkConnectedOverlayPositions]="overlayPositions"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
|
||||
[cdkConnectedOverlayScrollStrategy]="scrollStrategy"
|
||||
[cdkConnectedOverlayOffsetX]="-10"
|
||||
[cdkConnectedOverlayOffsetY]="18"
|
||||
[cdkConnectedOverlayFlexibleDimensions]="true"
|
||||
[cdkConnectedOverlayGrowAfterOpen]="true"
|
||||
[cdkConnectedOverlayPush]="true"
|
||||
cdkConnectedOverlayWidth="18.375rem"
|
||||
(backdropClick)="toggle()"
|
||||
>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Overlay, OverlayModule } from '@angular/cdk/overlay';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { DROPDOWN_OVERLAY_POSITIONS } from '@isa/ui/layout';
|
||||
import { FilterMenuComponent } from './filter-menu.component';
|
||||
import { FilterService } from '../../core';
|
||||
|
||||
@@ -30,6 +30,9 @@ export class FilterMenuButtonComponent {
|
||||
|
||||
selectedFilters = this.#filter.selectedFilterCount;
|
||||
|
||||
/** Standard overlay positions for the filter menu panel */
|
||||
readonly overlayPositions = DROPDOWN_OVERLAY_POSITIONS;
|
||||
|
||||
/**
|
||||
* Tracks the open state of the filter menu.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
[cdkConnectedOverlayDisableClose]="false"
|
||||
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
|
||||
[cdkConnectedOverlayMinWidth]="overlayMinWidth"
|
||||
[cdkConnectedOverlayLockPosition]="true"
|
||||
[cdkConnectedOverlayFlexibleDimensions]="true"
|
||||
[cdkConnectedOverlayGrowAfterOpen]="true"
|
||||
[cdkConnectedOverlayPush]="true"
|
||||
[cdkConnectedOverlayScrollStrategy]="blockScrollStrategy"
|
||||
(backdropClick)="close()"
|
||||
(detach)="isOpen.set(false)"
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronUp, isaActionChevronDown } from '@isa/icons';
|
||||
@@ -23,14 +21,15 @@ import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
|
||||
import {
|
||||
CdkConnectedOverlay,
|
||||
CdkOverlayOrigin,
|
||||
ConnectedPosition,
|
||||
ScrollStrategyOptions,
|
||||
} from '@angular/cdk/overlay';
|
||||
import { DropdownAppearance } from './dropdown.types';
|
||||
import { DropdownService } from './dropdown.service';
|
||||
import { CloseOnScrollDirective } from '@isa/ui/layout';
|
||||
import {
|
||||
CloseOnScrollDirective,
|
||||
DROPDOWN_OVERLAY_POSITIONS,
|
||||
} from '@isa/ui/layout';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
import { DropdownOptionComponent } from './dropdown-option.component';
|
||||
import { DropdownFilterComponent } from './dropdown-filter.component';
|
||||
import { DROPDOWN_HOST, DropdownHost } from './dropdown-host';
|
||||
@@ -99,80 +98,8 @@ export class DropdownButtonComponent<T>
|
||||
return this.#scrollStrategy.block();
|
||||
}
|
||||
|
||||
/** Offset in pixels between the trigger and the overlay panel */
|
||||
readonly #overlayOffset = 12;
|
||||
|
||||
/**
|
||||
* Position priority for the overlay panel.
|
||||
* Order: bottom-left, bottom-right, top-left, top-right,
|
||||
* right-top, right-bottom, left-top, left-bottom
|
||||
*/
|
||||
readonly overlayPositions: ConnectedPosition[] = [
|
||||
// Bottom left
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetY: this.#overlayOffset,
|
||||
},
|
||||
// Bottom right
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'bottom',
|
||||
overlayX: 'end',
|
||||
overlayY: 'top',
|
||||
offsetY: this.#overlayOffset,
|
||||
},
|
||||
// Top left
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
offsetY: -this.#overlayOffset,
|
||||
},
|
||||
// Top right
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'top',
|
||||
overlayX: 'end',
|
||||
overlayY: 'bottom',
|
||||
offsetY: -this.#overlayOffset,
|
||||
},
|
||||
// Right top
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetX: this.#overlayOffset,
|
||||
},
|
||||
// Right bottom
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
offsetX: this.#overlayOffset,
|
||||
},
|
||||
// Left top
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'top',
|
||||
overlayX: 'end',
|
||||
overlayY: 'top',
|
||||
offsetX: -this.#overlayOffset,
|
||||
},
|
||||
// Left bottom
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'bottom',
|
||||
overlayX: 'end',
|
||||
overlayY: 'bottom',
|
||||
offsetX: -this.#overlayOffset,
|
||||
},
|
||||
];
|
||||
/** Standard overlay positions for the dropdown panel */
|
||||
readonly overlayPositions = DROPDOWN_OVERLAY_POSITIONS;
|
||||
|
||||
appearance = input<DropdownAppearance>(DropdownAppearance.AccentOutline);
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './lib/breakpoint.directive';
|
||||
export * from './lib/breakpoint';
|
||||
export * from './lib/close-on-scroll.directive';
|
||||
export * from './lib/in-viewport.directive';
|
||||
export * from './lib/overlay-positions';
|
||||
|
||||
100
libs/ui/layout/src/lib/overlay-positions.ts
Normal file
100
libs/ui/layout/src/lib/overlay-positions.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ConnectedPosition } from '@angular/cdk/overlay';
|
||||
|
||||
/** Default offset in pixels between the trigger and the overlay panel */
|
||||
export const OVERLAY_OFFSET = 12;
|
||||
|
||||
/**
|
||||
* Creates standard dropdown overlay positions with configurable offset.
|
||||
*
|
||||
* Position priority for overlay panels:
|
||||
* CDK tries positions in order and picks the first one that fits in the viewport.
|
||||
*
|
||||
* With `flexibleDimensions=true` and `push=true`, the overlay will:
|
||||
* 1. Try positions in order
|
||||
* 2. Constrain size to fit in viewport (flexibleDimensions)
|
||||
* 3. Push into viewport if needed (push)
|
||||
*
|
||||
* Priority order (most to least preferred):
|
||||
* 1. Below the trigger (default, most natural for dropdowns)
|
||||
* 2. Above the trigger (when no space below)
|
||||
* 3. To the right (when no vertical space)
|
||||
* 4. To the left (last resort)
|
||||
*
|
||||
* @param offset - Offset in pixels between trigger and overlay (default: 12)
|
||||
* @returns Array of ConnectedPosition configurations
|
||||
*/
|
||||
export const createOverlayPositions = (
|
||||
offset: number = OVERLAY_OFFSET,
|
||||
): ConnectedPosition[] => [
|
||||
// Priority 1: Below trigger, left-aligned (default/preferred)
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetY: offset,
|
||||
},
|
||||
// Priority 2: Below trigger, right-aligned
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'bottom',
|
||||
overlayX: 'end',
|
||||
overlayY: 'top',
|
||||
offsetY: offset,
|
||||
},
|
||||
// Priority 3: Above trigger, left-aligned
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
offsetY: -offset,
|
||||
},
|
||||
// Priority 4: Above trigger, right-aligned
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'top',
|
||||
overlayX: 'end',
|
||||
overlayY: 'bottom',
|
||||
offsetY: -offset,
|
||||
},
|
||||
// Priority 5: Right of trigger, top-aligned
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetX: offset,
|
||||
},
|
||||
// Priority 6: Right of trigger, bottom-aligned
|
||||
{
|
||||
originX: 'end',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
offsetX: offset,
|
||||
},
|
||||
// Priority 7: Left of trigger, top-aligned
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'top',
|
||||
overlayX: 'end',
|
||||
overlayY: 'top',
|
||||
offsetX: -offset,
|
||||
},
|
||||
// Priority 8: Left of trigger, bottom-aligned
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'bottom',
|
||||
overlayX: 'end',
|
||||
overlayY: 'bottom',
|
||||
offsetX: -offset,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Standard dropdown overlay positions with default offset (12px).
|
||||
* Use this for most dropdown/menu components.
|
||||
*/
|
||||
export const DROPDOWN_OVERLAY_POSITIONS: ConnectedPosition[] =
|
||||
createOverlayPositions(OVERLAY_OFFSET);
|
||||
Reference in New Issue
Block a user