feat(checkout): add reward shopping cart and purchase options improvements

Add reward shopping cart item component and improve purchase options handling with branch resources and enhanced models.

## Changes

### Checkout Data Access

**New Models:**
- Branch: Type alias for BranchDTO
- Product: Type alias for ProductDTO
- ShoppingCartItem: Extended with required product and loyalty fields

**New Resources:**
- BranchResource: Resource for branch data management

**Service Improvements:**
- BranchService: Added return type and Branch model import
- PurchaseOptionsFacade: Added console logging for debugging

**Schema Updates:**
- base-schemas: Added new base schema definitions

### Reward Shopping Cart Feature

**New Component:**
- reward-shopping-cart-item: Individual cart item display component
  - Component, template, and styles
  - Integrated with cart items display

**Updated Components:**
- billing-and-shipping-address-card: Updated for reward flow
- reward-shopping-cart-items: Enhanced items list display

### Product Info Shared Components

**Updated Components:**
- destination-info: Improved destination display
- product-info-redemption: Enhanced redemption info display
- stock-info: Updated stock information display

### Remission Data Access

**New Resources:**
- stock.resource: Stock data resource management
- Added resources index export

**Service Improvements:**
- RemissionStockService: Updated implementation and tests

**Schema Updates:**
- fetch-stock-in-stock: Schema refinements

### Remission Features

**Updated Components:**
- remission-list: Component updates
- remission-instock.resource: Resource improvements
- instock.resource: Enhanced stock handling

### VSCode Settings

- Updated workspace settings

## Impact

- Reward shopping cart UI ready for use
- Improved type safety with new model definitions
- Better resource management for branches and stock
- Enhanced debugging with console logging
This commit is contained in:
Lorenz Hilpert
2025-10-06 17:14:29 +02:00
parent 1e9ac30b4d
commit d9940740ce
31 changed files with 480 additions and 173 deletions

View File

@@ -1,3 +1,4 @@
export * from './lib/errors';
export * from './lib/facades';
export * from './lib/models';
export * from './lib/schemas';

View File

@@ -23,10 +23,12 @@ export class PurchaseOptionsFacade {
}
addItem(params: AddItemToShoppingCartParams) {
console.log('Adding item to cart', params);
return this.#shoppingCartService.addItem(params);
}
updateItem(params: UpdateShoppingCartItemParams) {
console.log('Updating item in cart', params);
return this.#shoppingCartService.updateItem(params);
}

View File

@@ -0,0 +1,3 @@
import { BranchDTO } from '@generated/swagger/checkout-api';
export type Branch = BranchDTO;

View File

@@ -1,5 +1,6 @@
export * from './availability-type';
export * from './availability';
export * from './branch';
export * from './campaign';
export * from './checkout-item';
export * from './checkout';
@@ -12,6 +13,7 @@ export * from './order-options';
export * from './order-type';
export * from './order';
export * from './price';
export * from './product';
export * from './promotion';
export * from './shipping-address';
export * from './shipping-target';

View File

@@ -0,0 +1,3 @@
import { ProductDTO } from '@generated/swagger/checkout-api';
export type Product = ProductDTO;

View File

@@ -1,3 +1,8 @@
import { ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
export type ShoppingCartItem = ShoppingCartItemDTO;
import { ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
import { Loyalty } from './loyalty';
import { Product } from './product';
export type ShoppingCartItem = ShoppingCartItemDTO & {
product: Product;
loyalty: Loyalty;
};

View File

@@ -0,0 +1,40 @@
import { inject, Injectable, resource, signal } from '@angular/core';
import { BranchService } from '../services';
@Injectable()
export class BranchResource {
#branchService = inject(BranchService);
#params = signal<
{ branchId: number | null } | { branchNumber: string | null }
>({ branchId: null });
params(
params: { branchId: number | null } | { branchNumber: string | null },
) {
this.#params.set(params);
}
readonly resource = resource({
params: () => this.#params(),
loader: async ({ params, abortSignal }) => {
if ('branchNumber' in params && !params.branchNumber) {
return null;
}
if ('branchId' in params && !params.branchId) {
return null;
}
const res = await this.#branchService.fetchBranches(abortSignal);
return res.find((b) => {
if ('branchId' in params) {
return b.id === params.branchId;
} else {
return b.branchNumber === params.branchNumber;
}
});
},
});
}
@Injectable()
export class AssignedBranchResource {}

View File

@@ -1 +1,2 @@
export * from './branch.resource';
export * from './shopping-cart.resource';

View File

@@ -301,3 +301,87 @@ export const EntityDTOContainerOfDestinationDTOSchema = z
data: DestinationDTOSchema,
})
.optional();
// NotificationChannel is a bitwise enum
export const NotificationChannelSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
// EntityReferenceDTO schema
export const EntityReferenceDTOSchema = TouchedBaseSchema.extend({
pId: z.string().optional(),
reference: z
.object({
id: z.number().optional(),
pId: z.string().optional(),
})
.optional(),
source: z.number().optional(),
});
// AddresseeWithReferenceDTO schema
export const AddresseeWithReferenceDTOSchema = EntityReferenceDTOSchema.extend({
address: AddressSchema,
communicationDetails: CommunicationDetailsSchema,
firstName: z.string().optional(),
gender: GenderSchema,
lastName: z.string().optional(),
locale: z.string().optional(),
organisation: OrganisationSchema,
title: z.string().optional(),
});
// BuyerStatus and PayerStatus enum schemas (bitwise enums matching generated API)
export const BuyerStatusSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
export const PayerStatusSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
export const BuyerTypeSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
export const PayerTypeSchema = z.union([
z.literal(0),
z.literal(4),
z.literal(8),
z.literal(16),
]);
// BuyerDTO schema
export const BuyerDTOSchema = AddresseeWithReferenceDTOSchema.extend({
buyerNumber: z.string().optional(),
buyerStatus: BuyerStatusSchema.optional(),
buyerType: BuyerTypeSchema,
dateOfBirth: z.string().optional(),
isTemporaryAccount: z.boolean().optional(),
});
// PayerDTO schema
export const PayerDTOSchema = AddresseeWithReferenceDTOSchema.extend({
payerNumber: z.string().optional(),
payerStatus: PayerStatusSchema.optional(),
payerType: PayerTypeSchema,
});

View File

@@ -4,6 +4,7 @@ import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
import { Branch } from '../models';
@Injectable({ providedIn: 'root' })
export class BranchService {
@@ -12,7 +13,7 @@ export class BranchService {
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
@InFlight()
async fetchBranches(abortSignal?: AbortSignal) {
async fetchBranches(abortSignal?: AbortSignal): Promise<Branch[]> {
let req$ = this.#branchService.StoreCheckoutBranchGetBranches({});
if (abortSignal) {
@@ -26,5 +27,7 @@ export class BranchService {
this.#logger.error('Failed to fetch branches', error);
throw error;
}
return res.result as Branch[];
}
}

View File

@@ -1,3 +1,4 @@
export * from './branch.service';
export * from './checkout-metadata.service';
export * from './checkout.service';
export * from './shopping-cart.service';

View File

@@ -10,7 +10,7 @@ import {
} from '@isa/crm/data-access';
import { isaActionEdit } from '@isa/icons';
import { IconButtonComponent } from '@isa/ui/buttons';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { provideIcons } from '@ng-icons/core';
import { AddressComponent } from '@isa/shared/address';
@Component({
@@ -18,7 +18,7 @@ import { AddressComponent } from '@isa/shared/address';
templateUrl: './billing-and-shipping-address-card.component.html',
styleUrls: ['./billing-and-shipping-address-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [IconButtonComponent, NgIcon, AddressComponent],
imports: [IconButtonComponent, AddressComponent],
providers: [provideIcons({ isaActionEdit })],
})
export class BillingAndShippingAddressCardComponent {

View File

@@ -0,0 +1,3 @@
:host {
@apply flex flex-row gap-6 items-start p-6 bg-white rounded-2xl;
}

View File

@@ -0,0 +1,16 @@
@let itm = item();
<checkout-product-info-redemption
class="grow"
[item]="itm"
></checkout-product-info-redemption>
<div
class="flex flex-col justify-between shrink grow-0 self-stretch w-[14.25rem]"
>
<div class="flex justify-end mt-5">
<ui-icon-button name="isaActionClose" color="secondary"></ui-icon-button>
</div>
<div class="grow"></div>
<checkout-destination-info
[shoppingCartItem]="itm"
></checkout-destination-info>
</div>

View File

@@ -0,0 +1,33 @@
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
input,
} from '@angular/core';
import { ShoppingCartItem } from '@isa/checkout/data-access';
import {
ProductInfoRedemptionComponent,
DestinationInfoComponent,
} from '@isa/checkout/shared/product-info';
import { IconButtonComponent } from '@isa/ui/buttons';
import { provideIcons } from '@ng-icons/core';
import { isaActionClose } from '@isa/icons';
import { StockResource } from '@isa/remission/data-access';
@Component({
selector: 'checkout-reward-shopping-cart-item',
templateUrl: './reward-shopping-cart-item.component.html',
styleUrls: ['./reward-shopping-cart-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ProductInfoRedemptionComponent,
DestinationInfoComponent,
IconButtonComponent,
],
providers: [provideIcons({ isaActionClose })],
})
export class RewardShoppingCartItemComponent {
item = input.required<ShoppingCartItem>();
}

View File

@@ -0,0 +1,9 @@
@for (item of items(); track item.id) {
@defer (on viewport) {
<checkout-reward-shopping-cart-item
[item]="item"
></checkout-reward-shopping-cart-item>
} @placeholder {
<div>Item</div>
}
}

View File

@@ -1,9 +1,31 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core';
import {
SelectedRewardShoppingCartResource,
ShoppingCartItem,
} from '@isa/checkout/data-access';
import { RewardShoppingCartItemComponent } from '../reward-shopping-cart-item/reward-shopping-cart-item.component';
@Component({
selector: 'checkout-reward-shopping-cart-items',
templateUrl: './reward-shopping-cart-items.component.html',
styleUrls: ['./reward-shopping-cart-items.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RewardShoppingCartItemComponent],
})
export class RewardShoppingCartItemsComponent {}
export class RewardShoppingCartItemsComponent {
#rewardShoppingCartResource = inject(SelectedRewardShoppingCartResource);
items = computed(() => {
const cart = this.#rewardShoppingCartResource.resource.value();
return (
cart?.items
?.map((item) => item.data!)
.filter((item) => item.product != null) ?? []
);
});
}

View File

@@ -1,5 +1,13 @@
import { Component, computed, input } from '@angular/core';
import {
Component,
computed,
effect,
inject,
input,
linkedSignal,
} from '@angular/core';
import {
BranchResource,
getOrderTypeFeature,
OrderType,
ShoppingCartItem,
@@ -24,9 +32,12 @@ import { DatePipe } from '@angular/common';
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
}),
BranchResource,
],
})
export class DestinationInfoComponent {
#branchResource = inject(BranchResource);
shoppingCartItem =
input.required<
Pick<ShoppingCartItem, 'availability' | 'destination' | 'features'>
@@ -50,6 +61,8 @@ export class DestinationInfoComponent {
if (OrderType.InStore === orderType) {
return 'isaDeliveryRuecklage1';
}
return 'isaDeliveryVersand';
});
displayAddress = computed(() => {
@@ -57,14 +70,29 @@ export class DestinationInfoComponent {
return OrderType.InStore === orderType || OrderType.Pickup === orderType;
});
branchContainer = computed(
() => this.shoppingCartItem().destination?.data?.targetBranch,
);
branch = linkedSignal(
() => this.branchContainer()?.data ?? this.#branchResource.resource.value(),
);
branchChange = effect(() => {
const branchContainer = this.branchContainer();
if (branchContainer?.data && branchContainer?.id) {
this.#branchResource.params({ branchId: branchContainer.id });
} else {
this.#branchResource.params({ branchId: null, branchNumber: null });
}
});
branchName = computed(() => {
const destination = this.shoppingCartItem().destination;
return destination?.data?.targetBranch?.data?.name;
return this.branch()?.name || 'Filiale nicht gefunden';
});
address = computed(() => {
const destination = this.shoppingCartItem().destination;
console.log(destination?.data?.targetBranch?.data?.address);
return destination?.data?.targetBranch?.data?.address;
});

View File

@@ -1,44 +1,46 @@
@let prd = item().product;
@let rPoints = item().redemptionPoints;
<div class="grid grid-cols-[auto,1fr] gap-6">
<div>
<img
sharedProductRouterLink
sharedProductImage
[ean]="prd.ean"
[alt]="prd.name"
class="checkout-product-info-redemption__image w-14"
data-what="product-image"
/>
</div>
@let rPoints = points();
@if (prd) {
<div class="grid grid-cols-[auto,1fr] gap-6">
<div>
<img
sharedProductRouterLink
sharedProductImage
[ean]="prd.ean"
[alt]="prd.name"
class="checkout-product-info-redemption__image w-14"
data-what="product-image"
/>
</div>
<div class="flex flex-1 flex-col justify-between gap-1">
<div class="isa-text-body-2-bold">{{ prd.contributors }}</div>
<div
[class.isa-text-body-2-regular]="orientation() === 'horizontal'"
[class.isa-text-subtitle-1-regular]="orientation() === 'vertical'"
>
{{ prd.name }}
</div>
<div class="isa-text-body-2-regular">
<span class="isa-text-body-2-bold">{{ rPoints }}</span>
Lesepunkte
<div class="flex flex-1 flex-col justify-between gap-1">
<div class="isa-text-body-2-bold">{{ prd.contributors }}</div>
<div
[class.isa-text-body-2-regular]="orientation() === 'horizontal'"
[class.isa-text-subtitle-1-regular]="orientation() === 'vertical'"
>
{{ prd.name }}
</div>
<div class="isa-text-body-2-regular">
<span class="isa-text-body-2-bold">{{ rPoints }}</span>
Lesepunkte
</div>
</div>
</div>
</div>
<div
class="flex flex-1 flex-col justify-between gap-1"
[class.ml-20]="orientation() === 'vertical'"
>
<shared-product-format
[format]="prd.format"
[formatDetail]="prd.formatDetail"
[formatDetailsBold]="true"
></shared-product-format>
<div class="isa-text-body-2-regular text-neutral-600">
{{ prd.manufacturer }} | {{ prd.ean }}
<div
class="flex flex-1 flex-col justify-between gap-1"
[class.ml-20]="orientation() === 'vertical'"
>
<shared-product-format
[format]="prd.format"
[formatDetail]="prd.formatDetail"
[formatDetailsBold]="true"
></shared-product-format>
<div class="isa-text-body-2-regular text-neutral-600">
{{ prd.manufacturer }} | {{ prd.ean }}
</div>
<div class="isa-text-body-2-regular text-neutral-600">
{{ prd.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
</div>
<div class="isa-text-body-2-regular text-neutral-600">
{{ prd.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
</div>
}

View File

@@ -1,23 +1,44 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { Product } from '@isa/catalogue/data-access';
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import { Product as CatProduct } from '@isa/catalogue/data-access';
import { Product as CheckoutProduct } from '@isa/checkout/data-access';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { ProductFormatComponent } from '@isa/shared/product-foramt';
import { DatePipe } from '@angular/common';
import { Loyalty } from '@isa/checkout/data-access';
export type ProductInfoItem = {
product: Pick<
Product,
| 'ean'
| 'name'
| 'contributors'
| 'format'
| 'formatDetail'
| 'manufacturer'
| 'publicationDate'
>;
redemptionPoints?: number;
};
export type ProductInfoItem =
| {
product: Pick<
CatProduct,
| 'ean'
| 'name'
| 'contributors'
| 'format'
| 'formatDetail'
| 'manufacturer'
| 'publicationDate'
>;
redemptionPoints?: number;
}
| {
product: Pick<
CheckoutProduct,
| 'ean'
| 'name'
| 'contributors'
| 'format'
| 'formatDetail'
| 'manufacturer'
| 'publicationDate'
>;
loyalty: Pick<Loyalty, 'value'>;
};
@Component({
selector: 'checkout-product-info-redemption',
@@ -39,4 +60,17 @@ export class ProductInfoRedemptionComponent {
item = input.required<ProductInfoItem>();
orientation = input<'horizontal' | 'vertical'>('vertical');
points = computed(() => {
const item = this.item();
if ('redemptionPoints' in item) {
return item.redemptionPoints ?? 0;
}
if ('loyalty' in item && item.loyalty) {
return item.loyalty.value ?? 0;
}
return 0;
});
}

View File

@@ -25,6 +25,7 @@ export type StockInfoItem = {
standalone: true,
imports: [NgIconComponent, SkeletonLoaderComponent],
providers: [provideIcons({ isaFiliale })],
exportAs: 'stockInfo',
})
export class StockInfoComponent {
#stockService = inject(RemissionStockService);

View File

@@ -1,5 +1,6 @@
export * from './lib/services';
export * from './lib/models';
export * from './lib/stores';
export * from './lib/schemas';
export * from './lib/helpers';
export * from './lib/services';
export * from './lib/models';
export * from './lib/resources';
export * from './lib/stores';
export * from './lib/schemas';
export * from './lib/helpers';

View File

@@ -0,0 +1 @@
export * from './stock.resource';

View File

@@ -0,0 +1,20 @@
import { Injectable, inject, resource, signal } from '@angular/core';
import { RemissionStockService } from '../services';
import { FetchStockInStock } from '../schemas';
@Injectable({ providedIn: 'root' })
export class StockResource {
#stockService = inject(RemissionStockService);
#params = signal<FetchStockInStock>({ itemIds: [], stockId: undefined });
params(params: Partial<FetchStockInStock>) {
this.#params.update((current) => ({ ...current, ...params }));
}
readonly resource = resource({
params: () => this.#params(),
loader: async ({ params, abortSignal }) =>
this.#stockService.fetchStock(params, abortSignal),
});
}

View File

@@ -1,7 +1,7 @@
import { z } from 'zod';
export const FetchStockInStockSchema = z.object({
assignedStockId: z.number().optional(),
stockId: z.number().optional(),
itemIds: z.array(z.number()),
});

View File

@@ -100,10 +100,10 @@ describe('RemissionStockService', () => {
it('should handle abort signal by configuring the observable pipeline', async () => {
// Arrange
const abortController = new AbortController();
const pipeSpy = jest.fn().mockReturnValue(
of({ result: mockStock, error: false }),
);
const pipeSpy = jest
.fn()
.mockReturnValue(of({ result: mockStock, error: false }));
mockStockService.StockCurrentStock.mockReturnValue({
pipe: pipeSpy,
} as any);
@@ -212,16 +212,19 @@ describe('RemissionStockService', () => {
it('should handle abort signal by configuring the observable pipeline', async () => {
// Arrange
const abortController = new AbortController();
const pipeSpy = jest.fn().mockReturnValue(
of({ result: mockStockInfo, error: false }),
);
const pipeSpy = jest
.fn()
.mockReturnValue(of({ result: mockStockInfo, error: false }));
mockStockService.StockInStock.mockReturnValue({
pipe: pipeSpy,
} as any);
// Act
const result = await service.fetchStock(validParams, abortController.signal);
const result = await service.fetchStock(
validParams,
abortController.signal,
);
// Assert
expect(result).toEqual(mockStockInfo);

View File

@@ -112,8 +112,8 @@ export class RemissionStockService {
let assignedStockId: number;
if (parsed.assignedStockId) {
assignedStockId = parsed.assignedStockId;
if (parsed.stockId) {
assignedStockId = parsed.stockId;
} else {
assignedStockId = await this.fetchAssignedStock(abortSignal).then(
(s) => s.id,
@@ -121,7 +121,7 @@ export class RemissionStockService {
}
this.#logger.info('Fetching stock info from API', () => ({
stockId: parsed.assignedStockId,
stockId: parsed.stockId,
itemCount: parsed.itemIds.length,
}));

View File

@@ -47,7 +47,6 @@ 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 { RemissionProcessedHintComponent } from './remission-processed-hint/remission-processed-hint.component';
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';
@@ -92,7 +91,6 @@ function querySettingsFactory() {
IconButtonComponent,
StatefulButtonComponent,
RemissionListDepartmentElementsComponent,
RemissionProcessedHintComponent,
RemissionListEmptyStateComponent,
],
host: {

View File

@@ -1,39 +1,39 @@
import { inject, resource } from '@angular/core';
import { RemissionStockService } from '@isa/remission/data-access';
export const createRemissionInStockResource = (
params: () => {
itemIds: string[];
},
) => {
const remissionStockService = inject(RemissionStockService);
return resource({
params,
loader: async ({ abortSignal, params }) => {
if (!params?.itemIds || params.itemIds.length === 0) {
return;
}
const assignedStock =
await remissionStockService.fetchAssignedStock(abortSignal);
if (!assignedStock || !assignedStock.id) {
throw new Error('No current stock available');
}
const itemIds = params.itemIds.map((id) => Number(id));
if (itemIds.some((id) => isNaN(id))) {
throw new Error('Invalid Catalog Product Number provided');
}
return await remissionStockService.fetchStock(
{
itemIds,
assignedStockId: assignedStock.id,
},
abortSignal,
);
},
});
};
import { inject, resource } from '@angular/core';
import { RemissionStockService } from '@isa/remission/data-access';
export const createRemissionInStockResource = (
params: () => {
itemIds: string[];
},
) => {
const remissionStockService = inject(RemissionStockService);
return resource({
params,
loader: async ({ abortSignal, params }) => {
if (!params?.itemIds || params.itemIds.length === 0) {
return;
}
const assignedStock =
await remissionStockService.fetchAssignedStock(abortSignal);
if (!assignedStock || !assignedStock.id) {
throw new Error('No current stock available');
}
const itemIds = params.itemIds.map((id) => Number(id));
if (itemIds.some((id) => isNaN(id))) {
throw new Error('Invalid Catalog Product Number provided');
}
return await remissionStockService.fetchStock(
{
itemIds,
stockId: assignedStock.id,
},
abortSignal,
);
},
});
};

View File

@@ -1,39 +1,39 @@
import { inject, resource } from '@angular/core';
import { RemissionStockService } from '@isa/remission/data-access';
export const createInStockResource = (
params: () => {
itemIds: number[];
},
) => {
const remissionStockService = inject(RemissionStockService);
return resource({
params,
loader: async ({ abortSignal, params }) => {
if (!params?.itemIds || params.itemIds.length === 0) {
return;
}
const assignedStock =
await remissionStockService.fetchAssignedStock(abortSignal);
if (!assignedStock || !assignedStock.id) {
throw new Error('No current stock available');
}
const itemIds = params.itemIds;
if (itemIds.some((id) => isNaN(id))) {
throw new Error('Invalid Catalog Product Number provided');
}
return await remissionStockService.fetchStock(
{
itemIds,
assignedStockId: assignedStock.id,
},
abortSignal,
);
},
});
};
import { inject, resource } from '@angular/core';
import { RemissionStockService } from '@isa/remission/data-access';
export const createInStockResource = (
params: () => {
itemIds: number[];
},
) => {
const remissionStockService = inject(RemissionStockService);
return resource({
params,
loader: async ({ abortSignal, params }) => {
if (!params?.itemIds || params.itemIds.length === 0) {
return;
}
const assignedStock =
await remissionStockService.fetchAssignedStock(abortSignal);
if (!assignedStock || !assignedStock.id) {
throw new Error('No current stock available');
}
const itemIds = params.itemIds;
if (itemIds.some((id) => isNaN(id))) {
throw new Error('Invalid Catalog Product Number provided');
}
return await remissionStockService.fetchStock(
{
itemIds,
stockId: assignedStock.id,
},
abortSignal,
);
},
});
};