mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat(checkout): complete reward order confirmation with reusable product info component
- Extract reusable ProductInfoComponent from ProductInfoRedemptionComponent - Implement order confirmation item list with product, action card, and destination info - Add completed shopping carts tracking to checkout metadata service - Create Storybook stories for product info component variants - Update checkout completion orchestrator to store shopping cart data - Extract COMPLETED_SHOPPING_CARTS_METADATA_KEY constant for consistency
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductInfoComponent } from '@isa/checkout/shared/product-info';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
const mockProduct = {
|
||||
ean: '9783498007706',
|
||||
name: 'Die Assistentin',
|
||||
contributors: 'Wahl, Caroline',
|
||||
};
|
||||
|
||||
const meta: Meta<ProductInfoComponent> = {
|
||||
title: 'checkout/shared/product-info/ProductInfo',
|
||||
component: ProductInfoComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
provideRouter([{ path: ':ean', component: ProductInfoComponent }]),
|
||||
],
|
||||
}),
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductInfoComponent>;
|
||||
|
||||
export const BasicWithoutContent: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
argTypes: {
|
||||
item: { control: 'object' },
|
||||
nameSize: {
|
||||
control: { type: 'radio' },
|
||||
options: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallNameSize: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'small',
|
||||
},
|
||||
};
|
||||
|
||||
export const MediumNameSize: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeNameSize: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'large',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLesepunkte: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<checkout-product-info [item]="item" [nameSize]="nameSize">
|
||||
<div class="isa-text-body-2-regular">
|
||||
<span class="isa-text-body-2-bold">150</span> Lesepunkte
|
||||
</div>
|
||||
</checkout-product-info>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithManufacturer: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<checkout-product-info [item]="item" [nameSize]="nameSize">
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
Rowohlt Taschenbuch
|
||||
</div>
|
||||
</checkout-product-info>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithMultipleRows: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<checkout-product-info [item]="item" [nameSize]="nameSize">
|
||||
<div class="isa-text-body-2-regular">
|
||||
<span class="isa-text-body-2-bold">150</span> Lesepunkte
|
||||
</div>
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
Rowohlt Taschenbuch
|
||||
</div>
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
Erschienen: 01. Januar 2023
|
||||
</div>
|
||||
</checkout-product-info>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -9,3 +9,6 @@ export const CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY =
|
||||
|
||||
export const CHECKOUT_REWARD_SELECTION_POPUP_OPENED_STATE_KEY =
|
||||
'checkout-data-access.checkoutRewardSelectionPopupOpenedState';
|
||||
|
||||
export const COMPLETED_SHOPPING_CARTS_METADATA_KEY =
|
||||
'checkout-data-access.completedShoppingCarts';
|
||||
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
CHECKOUT_REWARD_SELECTION_POPUP_OPENED_STATE_KEY,
|
||||
CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY,
|
||||
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY,
|
||||
COMPLETED_SHOPPING_CARTS_METADATA_KEY,
|
||||
SELECTED_BRANCH_METADATA_KEY,
|
||||
} from '../constants';
|
||||
import z from 'zod';
|
||||
import { ShoppingCart } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CheckoutMetadataService {
|
||||
@@ -74,4 +76,20 @@ export class CheckoutMetadataService {
|
||||
this.#tabService.entityMap(),
|
||||
);
|
||||
}
|
||||
|
||||
getCompletedShoppingCarts(tabId: number): ShoppingCart[] | undefined {
|
||||
return getMetadataHelper(
|
||||
tabId,
|
||||
COMPLETED_SHOPPING_CARTS_METADATA_KEY,
|
||||
z.array(z.any()).optional(),
|
||||
this.#tabService.entityMap(),
|
||||
);
|
||||
}
|
||||
|
||||
addCompletedShoppingCart(tabId: number, shoppingCart: ShoppingCart) {
|
||||
const existingCarts = this.getCompletedShoppingCarts(tabId) || [];
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
[COMPLETED_SHOPPING_CARTS_METADATA_KEY]: [...existingCarts, shoppingCart],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply flex w-full items-start gap-6;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<div>Product Info</div>
|
||||
<checkout-product-info class="grow" [item]="productItem()" [nameSize]="'small'">
|
||||
<div class="isa-text-body-2-regular" data-what="product-points">
|
||||
<span class="isa-text-body-2-bold">{{ points() }}</span>
|
||||
Lesepunkte
|
||||
</div>
|
||||
<div class="isa-text-body-2-bold">{{ item()?.quantity }} x</div>
|
||||
</checkout-product-info>
|
||||
<checkout-confirmation-list-item-action-card
|
||||
class="w-[24.5rem]"
|
||||
[item]="item()"
|
||||
></checkout-confirmation-list-item-action-card>
|
||||
<div>Destination Info</div>
|
||||
<checkout-destination-info
|
||||
class="max-w-[22.9rem] grow"
|
||||
[shoppingCartItem]="shoppingCartItem()"
|
||||
></checkout-destination-info>
|
||||
|
||||
@@ -1,14 +1,74 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { DisplayOrderItem } from '@isa/oms/data-access';
|
||||
import { ConfirmationListItemActionCardComponent } from './confirmation-list-item-action-card/confirmation-list-item-action-card.component';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductInfoItem,
|
||||
DestinationInfoComponent,
|
||||
} from '@isa/checkout/shared/product-info';
|
||||
import { OrderConfiramtionStore } from '../../reward-order-confirmation.store';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-order-confirmation-item-list-item',
|
||||
templateUrl: './order-confirmation-item-list-item.component.html',
|
||||
styleUrls: ['./order-confirmation-item-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ConfirmationListItemActionCardComponent],
|
||||
imports: [
|
||||
ConfirmationListItemActionCardComponent,
|
||||
ProductInfoComponent,
|
||||
DestinationInfoComponent,
|
||||
],
|
||||
})
|
||||
export class OrderConfirmationItemListItemComponent {
|
||||
#orderConfiramtionStore = inject(OrderConfiramtionStore);
|
||||
|
||||
item = input.required<DisplayOrderItem>();
|
||||
|
||||
productItem = computed<ProductInfoItem>(() => {
|
||||
const product = this.item().product;
|
||||
|
||||
return {
|
||||
contributors: product?.contributors ?? '',
|
||||
ean: product?.ean ?? '',
|
||||
name: product?.name ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
points = computed(() => {
|
||||
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
|
||||
|
||||
if (!shoppingCart) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const item = this.item();
|
||||
|
||||
const shoppingCartItem = shoppingCart.items.find(
|
||||
(scItem) =>
|
||||
scItem?.data?.product?.catalogProductNumber ===
|
||||
item.product?.catalogProductNumber,
|
||||
)?.data;
|
||||
|
||||
return shoppingCartItem?.loyalty?.value ?? 0;
|
||||
});
|
||||
|
||||
shoppingCartItem = computed(() => {
|
||||
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
|
||||
const item = this.item();
|
||||
if (!shoppingCart) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return shoppingCart.items.find(
|
||||
(scItem) =>
|
||||
scItem?.data?.product?.catalogProductNumber ===
|
||||
item.product?.catalogProductNumber,
|
||||
)?.data;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:host {
|
||||
@apply block w-full;
|
||||
@apply flex flex-col gap-6 w-full;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
{{ orderType() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@for (item of items(); track item.id) {
|
||||
<checkout-order-confirmation-item-list-item
|
||||
[item]="item"
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { OrderConfirmationItemListItemComponent } from './order-confirmation-item-list-item/order-confirmation-item-list-item.component';
|
||||
import { DisplayOrder, DisplayOrderItem } from '@isa/oms/data-access';
|
||||
|
||||
import { getOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isaDeliveryRuecklage2,
|
||||
isaDeliveryRuecklage1,
|
||||
} from '@isa/icons';
|
||||
import { DisplayOrder } from '@isa/oms/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-order-confirmation-item-list',
|
||||
|
||||
@@ -12,8 +12,6 @@ import { OrderConfirmationAddressesComponent } from './order-confirmation-addres
|
||||
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.component';
|
||||
import { OrderConfirmationItemListComponent } from './order-confirmation-item-list/order-confirmation-item-list.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { JsonPipe } from '@angular/common';
|
||||
import { OmsMetadataService } from '@isa/oms/data-access';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { OrderConfiramtionStore } from './reward-order-confirmation.store';
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
deduplicateBranches,
|
||||
} from '@isa/crm/data-access';
|
||||
import { OmsMetadataService } from '@isa/oms/data-access';
|
||||
import { hasOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
|
||||
import {
|
||||
CheckoutMetadataService,
|
||||
hasOrderTypeFeature,
|
||||
OrderType,
|
||||
} from '@isa/checkout/data-access';
|
||||
import {
|
||||
patchState,
|
||||
signalStore,
|
||||
@@ -28,6 +32,7 @@ export const OrderConfiramtionStore = signalStore(
|
||||
withState(initialState),
|
||||
withProps(() => ({
|
||||
_omsMetadataService: inject(OmsMetadataService),
|
||||
_checkoutMetadataService: inject(CheckoutMetadataService),
|
||||
})),
|
||||
withComputed((state) => ({
|
||||
orders: computed(() => {
|
||||
@@ -44,10 +49,28 @@ export const OrderConfiramtionStore = signalStore(
|
||||
|
||||
const orders = state._omsMetadataService.getDisplayOrders(tabId);
|
||||
|
||||
return orders?.filter((order) => orderIds.includes(order.id!));
|
||||
return orders?.filter(
|
||||
(order) => order.id !== undefined && orderIds.includes(order.id),
|
||||
);
|
||||
}),
|
||||
})),
|
||||
withComputed((state) => ({
|
||||
shoppingCart: computed(() => {
|
||||
const tabId = state.tabId();
|
||||
const orders = state.orders();
|
||||
if (!tabId) {
|
||||
return undefined;
|
||||
}
|
||||
if (!orders || !orders.length) {
|
||||
return undefined;
|
||||
}
|
||||
const completedCarts =
|
||||
state._checkoutMetadataService.getCompletedShoppingCarts(tabId);
|
||||
|
||||
return completedCarts?.find(
|
||||
(cart) => cart.id === orders[0].shoppingCartId,
|
||||
);
|
||||
}),
|
||||
payers: computed(() => {
|
||||
const orders = state.orders();
|
||||
if (!orders) {
|
||||
|
||||
@@ -2,8 +2,12 @@ import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
ShoppingCartFacade,
|
||||
CompleteCrmOrderParams,
|
||||
CheckoutMetadataService,
|
||||
} from '@isa/checkout/data-access';
|
||||
import { OrderCreationFacade, OmsMetadataService } from '@isa/oms/data-access';
|
||||
import {
|
||||
OrderCreationFacade,
|
||||
OmsMetadataService,
|
||||
} from '@isa/oms/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import type { Order } from '@isa/checkout/data-access';
|
||||
|
||||
@@ -34,6 +38,7 @@ export class CheckoutCompletionOrchestratorService {
|
||||
#shoppingCartFacade = inject(ShoppingCartFacade);
|
||||
#orderCreationFacade = inject(OrderCreationFacade);
|
||||
#omsMetadataService = inject(OmsMetadataService);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
|
||||
/**
|
||||
* Complete checkout with CRM data and create orders.
|
||||
@@ -69,12 +74,27 @@ export class CheckoutCompletionOrchestratorService {
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
const shoppingCart = await this.#shoppingCartFacade.getShoppingCart(
|
||||
params.shoppingCartId,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
const orders =
|
||||
await this.#orderCreationFacade.createOrdersFromCheckout(checkoutId);
|
||||
|
||||
// Step 2: Update OMS metadata with created orders
|
||||
if (tabId && orders.length > 0) {
|
||||
this.#omsMetadataService.addDisplayOrders(tabId, orders);
|
||||
if (tabId && shoppingCart) {
|
||||
this.#checkoutMetadataService.addCompletedShoppingCart(
|
||||
tabId,
|
||||
shoppingCart,
|
||||
);
|
||||
}
|
||||
if (tabId && orders.length > 0 && shoppingCart) {
|
||||
this.#omsMetadataService.addDisplayOrders(
|
||||
tabId,
|
||||
orders,
|
||||
shoppingCart.id,
|
||||
);
|
||||
}
|
||||
|
||||
this.#logger.info(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './lib/destination-info/destination-info.component';
|
||||
export * from './lib/product-info/product-info.component';
|
||||
export * from './lib/product-info/product-info-redemption.component';
|
||||
export * from './lib/stock-info/stock-info.component';
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
@let prd = item().product;
|
||||
@let rPoints = points();
|
||||
@if (prd) {
|
||||
<div class="grid grid-cols-[auto,1fr] gap-6 items-start">
|
||||
<div>
|
||||
<img
|
||||
sharedProductRouterLink
|
||||
sharedProductImage
|
||||
[ean]="prd.ean"
|
||||
[alt]="prd.name"
|
||||
class="checkout-product-info-redemption__image w-14"
|
||||
data-what="product-image"
|
||||
/>
|
||||
<checkout-product-info [item]="baseItem()" [nameSize]="nameSize()">
|
||||
<div class="isa-text-body-2-regular" data-what="product-points">
|
||||
<span class="isa-text-body-2-bold">{{ rPoints }}</span>
|
||||
Lesepunkte
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col 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>
|
||||
</checkout-product-info>
|
||||
<div
|
||||
class="flex flex-1 flex-col gap-2"
|
||||
[class.ml-20]="orientation() === 'vertical'"
|
||||
@@ -39,11 +19,12 @@
|
||||
></shared-product-format>
|
||||
<div
|
||||
class="flex items-center gap-1 isa-text-body-2-regular text-neutral-600"
|
||||
data-what="product-manufacturer-ean"
|
||||
>
|
||||
<span class="truncate">{{ prd.manufacturer }}</span>
|
||||
<span class="shrink-0">| {{ prd.ean }}</span>
|
||||
</div>
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
<div class="isa-text-body-2-regular text-neutral-600" data-what="product-publication-date">
|
||||
{{ prd.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,15 @@ import {
|
||||
} 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';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductInfoItem,
|
||||
} from './product-info.component';
|
||||
|
||||
export type ProductInfoItem =
|
||||
export type ProductInfoRedemptionItem =
|
||||
| {
|
||||
product: Pick<
|
||||
CatProduct,
|
||||
@@ -46,18 +48,13 @@ export type ProductInfoItem =
|
||||
styleUrls: ['./product-info-redemption.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
ProductImageDirective,
|
||||
ProductRouterLinkDirective,
|
||||
ProductFormatComponent,
|
||||
DatePipe,
|
||||
],
|
||||
imports: [ProductInfoComponent, ProductFormatComponent, DatePipe],
|
||||
host: {
|
||||
'[class]': '[orientation()]',
|
||||
},
|
||||
})
|
||||
export class ProductInfoRedemptionComponent {
|
||||
item = input.required<ProductInfoItem>();
|
||||
item = input.required<ProductInfoRedemptionItem>();
|
||||
|
||||
orientation = input<'horizontal' | 'vertical'>('vertical');
|
||||
|
||||
@@ -73,4 +70,17 @@ export class ProductInfoRedemptionComponent {
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
nameSize = computed(() => {
|
||||
return this.orientation() === 'horizontal' ? 'small' : 'medium';
|
||||
});
|
||||
|
||||
baseItem = computed<ProductInfoItem>(() => {
|
||||
const item = this.item();
|
||||
return {
|
||||
ean: item.product.ean ?? '',
|
||||
name: item.product.name ?? '',
|
||||
contributors: item.product.contributors ?? '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block text-neutral-900;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@let i = item();
|
||||
<div class="grid grid-cols-[auto,1fr] gap-6 items-start">
|
||||
<div>
|
||||
<img
|
||||
sharedProductRouterLink
|
||||
sharedProductImage
|
||||
[ean]="i.ean"
|
||||
[alt]="i.name"
|
||||
class="checkout-product-info__image max-w-14 max-h-[5.125rem] object-contain"
|
||||
data-what="product-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<div class="isa-text-body-2-bold" data-what="product-contributors">
|
||||
{{ i.contributors }}
|
||||
</div>
|
||||
<div [class]="nameSizeClass()" data-what="product-name">
|
||||
{{ i.name }}
|
||||
</div>
|
||||
<ng-content />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,145 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ProductInfoComponent, ProductInfoItem } from './product-info.component';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
describe('ProductInfoComponent', () => {
|
||||
let component: ProductInfoComponent;
|
||||
let fixture: ComponentFixture<ProductInfoComponent>;
|
||||
|
||||
const mockProduct: ProductInfoItem = {
|
||||
ean: '9783161484100',
|
||||
name: 'Test Product Name',
|
||||
contributors: 'Test Author',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ProductInfoComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
provideProductImageUrl('https://test.example.com'),
|
||||
provideProductRouterLinkBuilder((ean: string) => `/product/${ean}`),
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(ProductInfoComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
fixture.componentRef.setInput('item', mockProduct);
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have item input', () => {
|
||||
fixture.componentRef.setInput('item', mockProduct);
|
||||
expect(component.item()).toEqual(mockProduct);
|
||||
});
|
||||
|
||||
it('should have default nameSize of medium', () => {
|
||||
fixture.componentRef.setInput('item', mockProduct);
|
||||
expect(component.nameSize()).toBe('medium');
|
||||
});
|
||||
|
||||
describe('nameSizeClass computation', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockProduct);
|
||||
});
|
||||
|
||||
it('should return isa-text-body-2-regular for small size', () => {
|
||||
fixture.componentRef.setInput('nameSize', 'small');
|
||||
expect(component.nameSizeClass()).toBe('isa-text-body-2-regular');
|
||||
});
|
||||
|
||||
it('should return isa-text-subtitle-1-regular for medium size', () => {
|
||||
fixture.componentRef.setInput('nameSize', 'medium');
|
||||
expect(component.nameSizeClass()).toBe('isa-text-subtitle-1-regular');
|
||||
});
|
||||
|
||||
it('should return isa-text-heading-3-regular for large size', () => {
|
||||
fixture.componentRef.setInput('nameSize', 'large');
|
||||
expect(component.nameSizeClass()).toBe('isa-text-heading-3-regular');
|
||||
});
|
||||
});
|
||||
|
||||
describe('template rendering', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockProduct);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render product contributors', () => {
|
||||
const contributorsEl: DebugElement = fixture.debugElement.query(
|
||||
By.css('[data-what="product-contributors"]')
|
||||
);
|
||||
expect(contributorsEl).toBeTruthy();
|
||||
expect(contributorsEl.nativeElement.textContent.trim()).toBe(
|
||||
'Test Author'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render product name with correct size class', () => {
|
||||
const nameEl: DebugElement = fixture.debugElement.query(
|
||||
By.css('[data-what="product-name"]')
|
||||
);
|
||||
expect(nameEl).toBeTruthy();
|
||||
expect(nameEl.nativeElement.textContent.trim()).toBe('Test Product Name');
|
||||
expect(nameEl.nativeElement.classList.contains('isa-text-subtitle-1-regular')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have E2E attribute on product image', () => {
|
||||
const imageEl: DebugElement = fixture.debugElement.query(
|
||||
By.css('[data-what="product-image"]')
|
||||
);
|
||||
expect(imageEl).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should project ng-content', () => {
|
||||
const customContentFixture = TestBed.createComponent(ProductInfoComponent);
|
||||
customContentFixture.componentRef.setInput('item', mockProduct);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'custom-content';
|
||||
div.textContent = 'Custom projected content';
|
||||
customContentFixture.nativeElement.appendChild(div);
|
||||
|
||||
customContentFixture.detectChanges();
|
||||
|
||||
const projectedContent = customContentFixture.nativeElement.querySelector('.custom-content');
|
||||
expect(projectedContent).toBeTruthy();
|
||||
expect(projectedContent.textContent).toBe('Custom projected content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('name size variations', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('item', mockProduct);
|
||||
});
|
||||
|
||||
it('should apply small name size class', () => {
|
||||
fixture.componentRef.setInput('nameSize', 'small');
|
||||
fixture.detectChanges();
|
||||
|
||||
const nameEl: DebugElement = fixture.debugElement.query(
|
||||
By.css('[data-what="product-name"]')
|
||||
);
|
||||
expect(nameEl.nativeElement.classList.contains('isa-text-body-2-regular')).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply large name size class', () => {
|
||||
fixture.componentRef.setInput('nameSize', 'large');
|
||||
fixture.detectChanges();
|
||||
|
||||
const nameEl: DebugElement = fixture.debugElement.query(
|
||||
By.css('[data-what="product-name"]')
|
||||
);
|
||||
expect(nameEl.nativeElement.classList.contains('isa-text-heading-3-regular')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
|
||||
export type ProductInfoItem = {
|
||||
ean: string;
|
||||
name: string;
|
||||
contributors: string;
|
||||
};
|
||||
|
||||
export type ProductNameSize = 'small' | 'medium' | 'large';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-product-info',
|
||||
templateUrl: './product-info.component.html',
|
||||
styleUrls: ['./product-info.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ProductImageDirective, ProductRouterLinkDirective],
|
||||
})
|
||||
export class ProductInfoComponent {
|
||||
item = input.required<ProductInfoItem>();
|
||||
|
||||
nameSize = input<ProductNameSize>('medium');
|
||||
|
||||
nameSizeClass = computed(() => {
|
||||
const size = this.nameSize();
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return 'isa-text-body-2-regular';
|
||||
case 'medium':
|
||||
return 'isa-text-subtitle-1-regular';
|
||||
case 'large':
|
||||
return 'isa-text-heading-3-regular';
|
||||
default:
|
||||
return 'isa-text-subtitle-1-regular';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
export * from './display-addressee.schema';
|
||||
export * from './display-branch.schema';
|
||||
export * from './display-logistician.schema';
|
||||
export * from './display-order-item.schema';
|
||||
export * from './display-order-payment.schema';
|
||||
export * from './display-order.schema';
|
||||
export * from './environment-channel.schema';
|
||||
export * from './fetch-return-details.schema';
|
||||
export * from './linked-record.schema';
|
||||
export * from './order-type.schema';
|
||||
export * from './price.schema';
|
||||
export * from './product.schema';
|
||||
export * from './promotion.schema';
|
||||
export * from './query-token.schema';
|
||||
export * from './return-process-question-answer.schema';
|
||||
export * from './return-receipt-values.schema';
|
||||
export * from './shipping-type.schema';
|
||||
export * from './terms-of-delivery.schema';
|
||||
export * from './type-of-delivery.schema';
|
||||
export * from './display-addressee.schema';
|
||||
export * from './display-branch.schema';
|
||||
export * from './display-logistician.schema';
|
||||
export * from './display-order-item.schema';
|
||||
export * from './display-order-payment.schema';
|
||||
export * from './display-order.schema';
|
||||
export * from './environment-channel.schema';
|
||||
export * from './fetch-return-details.schema';
|
||||
export * from './linked-record.schema';
|
||||
export * from './order-type.schema';
|
||||
export * from './price.schema';
|
||||
export * from './product.schema';
|
||||
export * from './promotion.schema';
|
||||
export * from './query-token.schema';
|
||||
export * from './return-process-question-answer.schema';
|
||||
export * from './return-receipt-values.schema';
|
||||
export * from './shipping-type.schema';
|
||||
export * from './terms-of-delivery.schema';
|
||||
export * from './type-of-delivery.schema';
|
||||
|
||||
@@ -8,19 +8,28 @@ import z from 'zod';
|
||||
export class OmsMetadataService {
|
||||
#tabService = inject(TabService);
|
||||
|
||||
getDisplayOrders(tabId: number): DisplayOrder[] | undefined {
|
||||
getDisplayOrders(tabId: number) {
|
||||
return getMetadataHelper(
|
||||
tabId,
|
||||
OMS_DISPLAY_ORDERS_KEY,
|
||||
z.array(DisplayOrderSchema).optional(),
|
||||
z
|
||||
.array(DisplayOrderSchema.extend({ shoppingCartId: z.number() }))
|
||||
.optional(),
|
||||
this.#tabService.entityMap(),
|
||||
);
|
||||
}
|
||||
|
||||
addDisplayOrders(tabId: number, orders: DisplayOrder[]) {
|
||||
addDisplayOrders(
|
||||
tabId: number,
|
||||
orders: DisplayOrder[],
|
||||
shoppingCartId: number,
|
||||
) {
|
||||
const existingOrders = this.getDisplayOrders(tabId) || [];
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
[OMS_DISPLAY_ORDERS_KEY]: [...existingOrders, ...orders],
|
||||
[OMS_DISPLAY_ORDERS_KEY]: [
|
||||
...existingOrders,
|
||||
...orders.map((order) => ({ ...order, shoppingCartId })),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user