mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
@@ -1,3 +1,4 @@
|
||||
export * from './lib/errors';
|
||||
export * from './lib/facades';
|
||||
export * from './lib/models';
|
||||
export * from './lib/schemas';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
3
libs/checkout/data-access/src/lib/models/branch.ts
Normal file
3
libs/checkout/data-access/src/lib/models/branch.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type Branch = BranchDTO;
|
||||
@@ -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';
|
||||
|
||||
3
libs/checkout/data-access/src/lib/models/product.ts
Normal file
3
libs/checkout/data-access/src/lib/models/product.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ProductDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type Product = ProductDTO;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 {}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './branch.resource';
|
||||
export * from './shopping-cart.resource';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './branch.service';
|
||||
export * from './checkout-metadata.service';
|
||||
export * from './checkout.service';
|
||||
export * from './shopping-cart.service';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply flex flex-row gap-6 items-start p-6 bg-white rounded-2xl;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) ?? []
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export type StockInfoItem = {
|
||||
standalone: true,
|
||||
imports: [NgIconComponent, SkeletonLoaderComponent],
|
||||
providers: [provideIcons({ isaFiliale })],
|
||||
exportAs: 'stockInfo',
|
||||
})
|
||||
export class StockInfoComponent {
|
||||
#stockService = inject(RemissionStockService);
|
||||
|
||||
@@ -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';
|
||||
|
||||
1
libs/remission/data-access/src/lib/resources/index.ts
Normal file
1
libs/remission/data-access/src/lib/resources/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './stock.resource';
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user