mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1974: feat(crm): introduce PrimaryCustomerCardResource and format-name utility
feat(crm): introduce PrimaryCustomerCardResource and format-name utility Replace SelectedCustomerBonusCardsResource with a new PrimaryCustomerCardResource that automatically loads and exposes the primary customer card as a signal. This simplifies customer card access across the application by providing a centralized, root-level injectable resource with automatic tab synchronization. Create new @isa/utils/format-name library to consolidate customer name formatting logic previously duplicated across components. The utility formats names with configurable first name, last name, and organization name fields. Key changes: - Add PrimaryCustomerCardResource as providedIn root service with automatic customer selection tracking via effect - Remove SelectedCustomerBonusCardsResource and its manual provisioning - Extract formatName function to dedicated utility library with Vitest setup - Update all reward-related components to use new resource pattern - Migrate OMS components to use centralized format-name utility - Add comprehensive unit tests for formatName function BREAKING CHANGE: SelectedCustomerBonusCardsResource has been removed Ref: #5389
This commit is contained in:
committed by
Lorenz Hilpert
parent
f549c59bc8
commit
0b76552211
@@ -4,10 +4,7 @@
|
||||
uiButton
|
||||
color="brand"
|
||||
size="large"
|
||||
[disabled]="
|
||||
!hasSelectedItems() ||
|
||||
(!!primaryBonusCard() && primaryBonusCardPoints() <= 0)
|
||||
"
|
||||
[disabled]="disableSelectRewardButton()"
|
||||
(click)="continueToPurchasingOptions()"
|
||||
>
|
||||
Prämie auswählen
|
||||
|
||||
@@ -15,10 +15,8 @@ import { PurchaseOptionsModalService } from '@modal/purchase-options';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { getRouteToCustomer } from '../helpers';
|
||||
import {
|
||||
getPrimaryBonusCard,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
} from '@isa/crm/data-access';
|
||||
import { PrimaryCustomerCardResource } from '@isa/crm/data-access';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
|
||||
@Component({
|
||||
selector: 'reward-action',
|
||||
@@ -32,30 +30,29 @@ export class RewardActionComponent {
|
||||
#store = inject(RewardCatalogStore);
|
||||
#tabId = injectTabId();
|
||||
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#purchasingOptionsModal = inject(PurchaseOptionsModalService);
|
||||
#shoppingCartFacade = inject(ShoppingCartFacade);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
|
||||
.resource;
|
||||
#primaryBonudCardResource = inject(PrimaryCustomerCardResource);
|
||||
|
||||
readonly customerCardResponseValue =
|
||||
this.#customerBonusCardsResource.value.asReadonly();
|
||||
|
||||
bonusCards = computed(() => {
|
||||
return this.customerCardResponseValue() ?? [];
|
||||
});
|
||||
|
||||
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
|
||||
primaryBonusCardPoints = computed(
|
||||
() => this.primaryBonusCard()?.totalPoints ?? 0,
|
||||
);
|
||||
readonly primaryCustomerCardValue =
|
||||
this.#primaryBonudCardResource.primaryCustomerCard;
|
||||
|
||||
selectedItems = computed(() => this.#store.selectedItems());
|
||||
|
||||
points = computed(() => this.primaryCustomerCardValue()?.totalPoints ?? 0);
|
||||
|
||||
hasSelectedItems = computed(() => {
|
||||
return Object.keys(this.selectedItems() || {}).length > 0;
|
||||
});
|
||||
|
||||
disableSelectRewardButton = computed(
|
||||
() =>
|
||||
!this.hasSelectedItems() ||
|
||||
(!!this.primaryCustomerCardValue() && this.points() <= 0),
|
||||
);
|
||||
|
||||
async continueToPurchasingOptions() {
|
||||
const tabId = this.#tabId();
|
||||
const items = Object.values(this.selectedItems() || {});
|
||||
@@ -93,8 +90,18 @@ export class RewardActionComponent {
|
||||
}
|
||||
|
||||
async #navigation(tabId: number) {
|
||||
if (!this.primaryBonusCard()) {
|
||||
// If no Customer Card -> No Customer Selected
|
||||
if (!this.primaryCustomerCardValue()) {
|
||||
const route = getRouteToCustomer(tabId);
|
||||
|
||||
// Preserve context: Store current reward page URL to return to after customer selection
|
||||
await this.#navigationState.preserveContext(
|
||||
{
|
||||
returnUrl: this.#router.url,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
|
||||
await this.#router.navigate(route.path, {
|
||||
queryParams: route.queryParams,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { RewardListComponent } from './reward-list/reward-list.component';
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
import { RewardActionComponent } from './reward-action/reward-action.component';
|
||||
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
|
||||
import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
import { SelectedCustomerResource } from '@isa/crm/data-access';
|
||||
|
||||
/**
|
||||
* Factory function to retrieve query settings from the activated route data.
|
||||
@@ -35,7 +35,7 @@ function querySettingsFactory() {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
SelectedCustomerResource,
|
||||
provideFilter(
|
||||
withQuerySettingsFactory(querySettingsFactory),
|
||||
withQueryParamsSync(),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<div class="flex flex-row gap-20 text-isa-neutral-900">
|
||||
<div class="flex flex-col gap-[0.125rem]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="isa-text-body-1-regular"
|
||||
>{{ primaryBonusCard()?.firstName }} {{ primaryBonusCard()?.lastName }}
|
||||
</span>
|
||||
<span class="isa-text-body-1-regular">{{ customerName() }} </span>
|
||||
<span class="isa-text-body-1-bold"
|
||||
>{{ primaryBonusCardPoints() }} Lesepunkte</span
|
||||
>
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
CrmTabMetadataService,
|
||||
getPrimaryBonusCard,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
PrimaryCustomerCardResource,
|
||||
SelectedCustomerResource,
|
||||
} from '@isa/crm/data-access';
|
||||
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
|
||||
import {
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@isa/checkout/data-access';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { formatName } from '@isa/utils/format-name';
|
||||
|
||||
@Component({
|
||||
selector: 'reward-customer-card',
|
||||
@@ -34,20 +35,17 @@ export class RewardCustomerCardComponent {
|
||||
#crmTabMetadataService = inject(CrmTabMetadataService);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
tabId = injectTabId();
|
||||
#customerResource = inject(SelectedCustomerResource).resource;
|
||||
#shoppingCartResource = inject(SelectedRewardShoppingCartResource).resource;
|
||||
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
|
||||
.resource;
|
||||
#primaryCustomerCardResource = inject(PrimaryCustomerCardResource);
|
||||
|
||||
readonly customerCardResponseValue =
|
||||
this.#customerBonusCardsResource.value.asReadonly();
|
||||
readonly customerValue = this.#customerResource.value.asReadonly();
|
||||
|
||||
bonusCards = computed(() => {
|
||||
return this.customerCardResponseValue() ?? [];
|
||||
});
|
||||
readonly primaryCustomerCardValue =
|
||||
this.#primaryCustomerCardResource.primaryCustomerCard;
|
||||
|
||||
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
|
||||
primaryBonusCardPoints = computed(
|
||||
() => this.primaryBonusCard()?.totalPoints ?? 0,
|
||||
() => this.primaryCustomerCardValue()?.totalPoints ?? 0,
|
||||
);
|
||||
|
||||
readonly shoppingCartResponseValue =
|
||||
@@ -58,6 +56,15 @@ export class RewardCustomerCardComponent {
|
||||
() => this.shoppingCartResponseValue()?.items?.length ?? 0,
|
||||
);
|
||||
|
||||
customerName = computed(() => {
|
||||
const customer = this.customerValue();
|
||||
return formatName({
|
||||
firstName: customer?.firstName,
|
||||
lastName: customer?.lastName,
|
||||
organisationName: customer?.organisation?.name,
|
||||
});
|
||||
});
|
||||
|
||||
resetCustomerAndCart() {
|
||||
this.#crmTabMetadataService.setSelectedCustomerId(this.tabId()!, undefined);
|
||||
this.#checkoutMetadataService.setRewardShoppingCartId(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@let card = primaryBonusCard();
|
||||
@let cardFetching = customerCardResponseFetching();
|
||||
@let card = primaryCustomerCardValue();
|
||||
@let cardFetching = primaryCustomerCardFetching();
|
||||
@if (!cardFetching) {
|
||||
@if (card) {
|
||||
<reward-customer-card></reward-customer-card>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { RewardStartCardComponent } from './reward-start-card/reward-start-card.component';
|
||||
import { RewardCustomerCardComponent } from './reward-customer-card/reward-customer-card.component';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
getPrimaryBonusCard,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
PrimaryCustomerCardResource,
|
||||
} from '@isa/crm/data-access';
|
||||
@Component({
|
||||
selector: 'reward-header',
|
||||
@@ -23,17 +21,10 @@ import {
|
||||
],
|
||||
})
|
||||
export class RewardHeaderComponent {
|
||||
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
|
||||
.resource;
|
||||
#primaryCustomerCardResource = inject(PrimaryCustomerCardResource);
|
||||
|
||||
readonly customerCardResponseValue =
|
||||
this.#customerBonusCardsResource.value.asReadonly();
|
||||
readonly customerCardResponseFetching =
|
||||
this.#customerBonusCardsResource.isLoading;
|
||||
|
||||
bonusCards = computed(() => {
|
||||
return this.customerCardResponseValue() ?? [];
|
||||
});
|
||||
|
||||
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
|
||||
readonly primaryCustomerCardValue =
|
||||
this.#primaryCustomerCardResource.primaryCustomerCard;
|
||||
readonly primaryCustomerCardFetching =
|
||||
this.#primaryCustomerCardResource.loading;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
input,
|
||||
linkedSignal,
|
||||
inject,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -17,7 +18,7 @@ import { RewardCatalogStore } from '@isa/checkout/data-access';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, CheckboxComponent],
|
||||
})
|
||||
export class RewardListItemSelectComponent {
|
||||
export class RewardListItemSelectComponent implements OnDestroy {
|
||||
#store = inject(RewardCatalogStore);
|
||||
|
||||
item = input.required<Item>();
|
||||
@@ -36,4 +37,9 @@ export class RewardListItemSelectComponent {
|
||||
this.#store.removeItem(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
const itemId = this.item().id;
|
||||
this.#store.removeItem(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="isa-text-body-1-bold">
|
||||
<h4 class="isa-text-body-1-regular mb-1">Rechnugsadresse</h4>
|
||||
<h4 class="isa-text-body-1-regular mb-1">Rechnungsadresse</h4>
|
||||
<div>
|
||||
@if (payer(); as payer) {
|
||||
<div>{{ payerName() }}</div>
|
||||
@@ -30,6 +30,5 @@
|
||||
size="large"
|
||||
color="secondary"
|
||||
type="button"
|
||||
>
|
||||
</button>
|
||||
></button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="info-block">
|
||||
<div class="info-block--label">{{ customerName() }}</div>
|
||||
<div class="info-block--value">{{ bonusCardPoints() }} Lesepunkte</div>
|
||||
<div class="info-block--value">{{ primaryBonusCardPoints() }} Lesepunkte</div>
|
||||
</div>
|
||||
<div class="info-block">
|
||||
<div class="info-block--label">Prämien ausgewählt</div>
|
||||
@@ -13,6 +13,6 @@
|
||||
<div class="info-block">
|
||||
<div class="info-block--label">Verbleibende Lesepunkte</div>
|
||||
<div class="info-block--value">
|
||||
{{ bonusCardPoints() - totalPointsRequired() }} Lesepunkte
|
||||
{{ primaryBonusCardPoints() - totalPointsRequired() }} Lesepunkte
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
|
||||
import {
|
||||
SelectedCustomerResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
getPrimaryBonusCard,
|
||||
PrimaryCustomerCardResource,
|
||||
} from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
@@ -21,8 +20,7 @@ import {
|
||||
export class CheckoutCustomerRewardCardComponent {
|
||||
#shoppingCartResource = inject(SelectedRewardShoppingCartResource).resource;
|
||||
#customerResource = inject(SelectedCustomerResource).resource;
|
||||
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
|
||||
.resource;
|
||||
#primaryCustomerCardResource = inject(PrimaryCustomerCardResource);
|
||||
|
||||
customerLoading = this.#customerResource.isLoading;
|
||||
|
||||
@@ -51,14 +49,12 @@ export class CheckoutCustomerRewardCardComponent {
|
||||
return loyalty.reduce((a, b) => a + b, 0);
|
||||
});
|
||||
|
||||
bonusCardsLoading = this.#customerBonusCardsResource.isLoading;
|
||||
bonusCardsLoading = this.#primaryCustomerCardResource.loading;
|
||||
|
||||
bonusCards = computed(() => {
|
||||
return this.#customerBonusCardsResource.value() ?? [];
|
||||
});
|
||||
readonly primaryCustomerCardValue =
|
||||
this.#primaryCustomerCardResource.primaryCustomerCard;
|
||||
|
||||
bonusCardPoints = computed(() => {
|
||||
const primary = getPrimaryBonusCard(this.bonusCards());
|
||||
return primary?.totalPoints ?? 0;
|
||||
});
|
||||
primaryBonusCardPoints = computed(
|
||||
() => this.primaryCustomerCardValue()?.totalPoints ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { RewardShoppingCartItemsComponent } from './reward-shopping-cart-items/r
|
||||
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
|
||||
import {
|
||||
SelectedCustomerResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
} from '@isa/crm/data-access';
|
||||
import { CompleteOrderButtonComponent } from './complete-order-button/complete-order-button.component';
|
||||
import { RewardSelectionTriggerComponent } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
@@ -27,7 +26,6 @@ import { RewardSelectionTriggerComponent } from '@isa/checkout/shared/reward-sel
|
||||
providers: [
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedCustomerResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
],
|
||||
})
|
||||
export class RewardShoppingCartComponent {}
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
} from '@isa/checkout/data-access';
|
||||
import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-checkout',
|
||||
@@ -62,7 +61,6 @@ import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
// Required providers
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
],
|
||||
@@ -92,7 +90,6 @@ import {
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
} from '@isa/checkout/data-access';
|
||||
import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'app-advanced',
|
||||
@@ -106,7 +103,6 @@ import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
providers: [
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
RewardSelectionService,
|
||||
],
|
||||
})
|
||||
@@ -180,7 +176,6 @@ When using `RewardSelectionService` or `RewardSelectionPopUpService` directly, p
|
||||
providers: [
|
||||
SelectedShoppingCartResource, // Regular cart data
|
||||
SelectedRewardShoppingCartResource, // Reward cart data
|
||||
SelectedCustomerBonusCardsResource, // Customer bonus cards
|
||||
RewardSelectionService, // Core service
|
||||
RewardSelectionPopUpService, // Optional: only if using pop-up
|
||||
]
|
||||
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
RewardSelectionDialogComponent,
|
||||
RewardSelectionDialogResult,
|
||||
} from '../reward-selection-dialog.component';
|
||||
import {
|
||||
getPrimaryBonusCard,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
} from '@isa/crm/data-access';
|
||||
import { PrimaryCustomerCardResource } from '@isa/crm/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { RewardSelectionItem } from '@isa/checkout/data-access';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
@@ -27,11 +24,10 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class RewardSelectionService {
|
||||
priceAndRedemptionPointsResource = inject(PriceAndRedemptionPointsResource);
|
||||
rewardSelectionDialog = injectDialog(RewardSelectionDialogComponent);
|
||||
|
||||
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
|
||||
.resource;
|
||||
#primaryCustomerCardResource = inject(PrimaryCustomerCardResource);
|
||||
#priceAndRedemptionPointsResource = inject(PriceAndRedemptionPointsResource);
|
||||
#shoppingCartResource = inject(SelectedShoppingCartResource).resource;
|
||||
#rewardShoppingCartResource = inject(SelectedRewardShoppingCartResource)
|
||||
.resource;
|
||||
@@ -42,11 +38,11 @@ export class RewardSelectionService {
|
||||
readonly rewardShoppingCartResponseValue =
|
||||
this.#rewardShoppingCartResource.value.asReadonly();
|
||||
|
||||
readonly customerCardResponseValue =
|
||||
this.#customerBonusCardsResource.value.asReadonly();
|
||||
readonly primaryCustomerCardValue =
|
||||
this.#primaryCustomerCardResource.primaryCustomerCard;
|
||||
|
||||
readonly priceAndRedemptionPoints =
|
||||
this.priceAndRedemptionPointsResource.priceAndRedemptionPoints;
|
||||
this.#priceAndRedemptionPointsResource.priceAndRedemptionPoints;
|
||||
|
||||
shoppingCartItems = computed(() => {
|
||||
return (
|
||||
@@ -104,21 +100,16 @@ export class RewardSelectionService {
|
||||
});
|
||||
});
|
||||
|
||||
bonusCards = computed(() => {
|
||||
return this.customerCardResponseValue() ?? [];
|
||||
});
|
||||
|
||||
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
|
||||
primaryBonusCardPoints = computed(
|
||||
() => this.primaryBonusCard()?.totalPoints ?? 0,
|
||||
() => this.primaryCustomerCardValue()?.totalPoints ?? 0,
|
||||
);
|
||||
|
||||
isLoading = computed(
|
||||
() =>
|
||||
this.#shoppingCartResource.isLoading() ||
|
||||
this.#rewardShoppingCartResource.isLoading() ||
|
||||
this.#customerBonusCardsResource.isLoading() ||
|
||||
this.priceAndRedemptionPointsResource.loading(),
|
||||
this.#primaryCustomerCardResource.loading() ||
|
||||
this.#priceAndRedemptionPointsResource.loading(),
|
||||
);
|
||||
#isLoading$ = toObservable(this.isLoading);
|
||||
|
||||
@@ -133,7 +124,7 @@ export class RewardSelectionService {
|
||||
);
|
||||
|
||||
canOpen = computed(
|
||||
() => this.eligibleItems().length > 0 && !!this.primaryBonusCard(),
|
||||
() => this.eligibleItems().length > 0 && !!this.primaryBonusCardPoints(),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -141,9 +132,11 @@ export class RewardSelectionService {
|
||||
const items = this.selectionItemsWithOrderType();
|
||||
|
||||
untracked(() => {
|
||||
const resourceLoading = this.priceAndRedemptionPointsResource.loading();
|
||||
const resourceLoading =
|
||||
this.#priceAndRedemptionPointsResource.loading();
|
||||
|
||||
if (!resourceLoading && items.length > 0) {
|
||||
this.priceAndRedemptionPointsResource.loadPriceAndRedemptionPoints(
|
||||
this.#priceAndRedemptionPointsResource.loadPriceAndRedemptionPoints(
|
||||
items,
|
||||
);
|
||||
}
|
||||
@@ -157,7 +150,6 @@ export class RewardSelectionService {
|
||||
closeText: string;
|
||||
}): Promise<RewardSelectionDialogResult> {
|
||||
const rewardSelectionItems = this.eligibleItems();
|
||||
console.log(rewardSelectionItems);
|
||||
const dialogRef = this.rewardSelectionDialog({
|
||||
title: 'Ein oder mehrere Artikel sind als Prämie verfügbar',
|
||||
data: {
|
||||
@@ -185,12 +177,11 @@ export class RewardSelectionService {
|
||||
|
||||
async reloadResources(): Promise<void> {
|
||||
// Start reloading all resources
|
||||
// Note: Price and redemption points will be loaded automatically by the effect
|
||||
// Note: PrimaryCustomerCard, Price and redemption points will be loaded automatically by the effect
|
||||
// when selectionItemsWithOrderType changes after cart resources are reloaded
|
||||
await Promise.all([
|
||||
this.#shoppingCartResource.reload(),
|
||||
this.#rewardShoppingCartResource.reload(),
|
||||
this.#customerBonusCardsResource.reload(),
|
||||
]);
|
||||
|
||||
// Wait until all resources are fully loaded (isLoading becomes false)
|
||||
|
||||
@@ -6,7 +6,6 @@ import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { injectFeedbackDialog } from '@isa/ui/dialog';
|
||||
import { RewardSelectionService } from '../service/reward-selection.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
|
||||
import {
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedShoppingCartResource,
|
||||
@@ -21,7 +20,6 @@ import { CheckoutNavigationService } from '@shared/services/navigation';
|
||||
providers: [
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedCustomerBonusCardsResource,
|
||||
RewardSelectionService,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { effect, inject, Injectable, resource, signal } from '@angular/core';
|
||||
import { CrmSearchService, CrmTabMetadataService } from '../services';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerBonusCardsResource {
|
||||
#customerService = inject(CrmSearchService);
|
||||
|
||||
#params = signal<{ customerId: number | undefined }>({
|
||||
customerId: undefined,
|
||||
});
|
||||
|
||||
params(params: { customerId?: number }) {
|
||||
this.#params.update((p) => ({ ...p, ...params }));
|
||||
}
|
||||
|
||||
readonly resource = resource({
|
||||
params: () => this.#params(),
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
if (!params.customerId) {
|
||||
return undefined;
|
||||
}
|
||||
const res = await this.#customerService.fetchCustomerCards(
|
||||
{
|
||||
customerId: params.customerId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
return res.result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SelectedCustomerBonusCardsResource extends CustomerBonusCardsResource {
|
||||
#tabId = inject(TabService).activatedTabId;
|
||||
#customerMetadata = inject(CrmTabMetadataService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
effect(() => {
|
||||
const tabId = this.#tabId();
|
||||
const customerId = tabId
|
||||
? this.#customerMetadata.selectedCustomerId(tabId)
|
||||
: undefined;
|
||||
this.params({ customerId });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './country.resource';
|
||||
export * from './customer-bonus-cards.resource';
|
||||
export * from './primary-customer-card.resource';
|
||||
export * from './customer-shipping-address.resource';
|
||||
export * from './customer-shipping-addresses.resource';
|
||||
export * from './customer.resource';
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
Injectable,
|
||||
resource,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { CrmSearchService, CrmTabMetadataService } from '../services';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { BonusCardInfo } from '../models';
|
||||
import { getPrimaryBonusCard } from '../helpers';
|
||||
|
||||
/**
|
||||
* Resource for the primary customer card of a customer.
|
||||
*
|
||||
* This resource automatically loads the primary bonus card of a customer
|
||||
* as soon as a `customerId` is set in the tab metadata. An internal effect
|
||||
* monitors changes to the `selectedCustomerId` in the active tab and
|
||||
* triggers loading of the customer card automatically.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const resource = inject(PrimaryCustomerCardResource);
|
||||
*
|
||||
* // Primary customer card (automatically loaded via effect)
|
||||
* const card = resource.primaryCustomerCard();
|
||||
*
|
||||
* // Loading status
|
||||
* const isLoading = resource.loading();
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PrimaryCustomerCardResource {
|
||||
#customerMetadata = inject(CrmTabMetadataService);
|
||||
#customerService = inject(CrmSearchService);
|
||||
#tabId = injectTabId();
|
||||
|
||||
#customerId = signal<number | undefined>(undefined);
|
||||
|
||||
#primaryCustomerCardResource = resource({
|
||||
params: computed(() => ({ customerId: this.#customerId() })),
|
||||
loader: async ({
|
||||
params,
|
||||
abortSignal,
|
||||
}): Promise<BonusCardInfo | undefined> => {
|
||||
const tabId = this.#tabId();
|
||||
if (!tabId || !params.customerId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await this.#customerService.fetchCustomerCards(
|
||||
{ customerId: params.customerId },
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
if (res?.result?.length > 0) {
|
||||
return getPrimaryBonusCard(res.result);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
defaultValue: undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal containing the primary customer card of the selected customer.
|
||||
* Automatically updated when the `selectedCustomerId` changes in the tab metadata.
|
||||
*/
|
||||
readonly primaryCustomerCard =
|
||||
this.#primaryCustomerCardResource.value.asReadonly();
|
||||
|
||||
/**
|
||||
* Signal indicating whether the customer card is currently being loaded.
|
||||
*/
|
||||
readonly loading = this.#primaryCustomerCardResource.isLoading;
|
||||
|
||||
/**
|
||||
* Signal containing an error message if an error occurred during loading.
|
||||
* Returns `null` if no error is present.
|
||||
*/
|
||||
readonly error = computed(
|
||||
() => this.#primaryCustomerCardResource.error()?.message ?? null,
|
||||
);
|
||||
|
||||
/**
|
||||
* Loads the primary customer card for the specified customer ID.
|
||||
*
|
||||
* @param customerId - The ID of the customer whose primary card should be loaded
|
||||
*/
|
||||
loadPrimaryCustomerCard(customerId: number | undefined) {
|
||||
this.#customerId.set(customerId);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* Effect: Monitors changes to the `selectedCustomerId` in the tab metadata
|
||||
* and automatically loads the primary customer card whenever a new customer ID
|
||||
* is set. This ensures that the customer card is always in sync with the
|
||||
* customer selection in the active tab.
|
||||
*
|
||||
* Uses `untracked` to prevent creating additional signal dependencies when
|
||||
* checking if the customer ID has actually changed, avoiding unnecessary reloads.
|
||||
*/
|
||||
effect(() => {
|
||||
const tabId = this.#tabId();
|
||||
let customerId = undefined;
|
||||
|
||||
if (tabId) {
|
||||
customerId = this.#customerMetadata.selectedCustomerId(tabId);
|
||||
}
|
||||
|
||||
untracked(() => {
|
||||
if (this.#customerId() !== customerId) {
|
||||
this.loadPrimaryCustomerCard(customerId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { isaActionChevronDown, isaNavigationKunden } from '@isa/icons';
|
||||
import { InfoButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { formatName } from 'libs/oms/utils/format-name';
|
||||
import { formatName } from '@isa/utils/format-name';
|
||||
import { UiMenu } from '@isa/ui/menu';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CdkMenuTrigger } from '@angular/cdk/menu';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ReturnSearchResultItemComponent } from './return-search-result-item.component';
|
||||
import { ReceiptListItem } from '@isa/oms/data-access';
|
||||
import { formatName } from 'libs/oms/utils/format-name';
|
||||
import { formatName } from '@isa/utils/format-name';
|
||||
|
||||
jest.mock('libs/oms/utils/format-name', () => ({
|
||||
jest.mock('@isa/utils/format-name', () => ({
|
||||
formatName: jest.fn(() => 'Formatted Name'),
|
||||
}));
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { ReceiptListItem } from '@isa/oms/data-access';
|
||||
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import { formatName } from 'libs/oms/utils/format-name';
|
||||
import { formatName } from '@isa/utils/format-name';
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-search-result-item',
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// TODO: Create proper library or add to existing one - or we will run into an import issue!!
|
||||
export function formatName({
|
||||
firstName,
|
||||
lastName,
|
||||
organisationName,
|
||||
}: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
organisationName?: string;
|
||||
}) {
|
||||
const nameCombined = [lastName, firstName].filter((f) => !!f);
|
||||
const organisation = [organisationName].filter((f) => !!f);
|
||||
|
||||
return [organisation.join(), nameCombined.join(' ')]
|
||||
.filter((f) => !!f)
|
||||
.join(' - ');
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './format-name.function';
|
||||
7
libs/utils/format-name/README.md
Normal file
7
libs/utils/format-name/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# format-name
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test format-name` to execute the unit tests.
|
||||
34
libs/utils/format-name/eslint.config.cjs
Normal file
34
libs/utils/format-name/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/utils/format-name/project.json
Normal file
20
libs/utils/format-name/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "format-name",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/utils/format-name/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/utils/format-name"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/utils/format-name/src/index.ts
Normal file
1
libs/utils/format-name/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/format-name/format-name.component';
|
||||
@@ -0,0 +1,125 @@
|
||||
import { formatName } from './format-name.component';
|
||||
|
||||
describe('formatName', () => {
|
||||
it('should format full name with first and last name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('Doe John');
|
||||
});
|
||||
|
||||
it('should format name with organisation and full name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
organisationName: 'Acme Corp',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('Acme Corp - Doe John');
|
||||
});
|
||||
|
||||
it('should format with only organisation name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
organisationName: 'Acme Corp',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('Acme Corp');
|
||||
});
|
||||
|
||||
it('should format with only last name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
lastName: 'Doe',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('Doe');
|
||||
});
|
||||
|
||||
it('should format with only first name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('John');
|
||||
});
|
||||
|
||||
it('should return empty string when all inputs are undefined', () => {
|
||||
// Arrange
|
||||
const input = {};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when all inputs are empty strings', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
organisationName: '',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should format with organisation and only last name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
lastName: 'Doe',
|
||||
organisationName: 'Acme Corp',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('Acme Corp - Doe');
|
||||
});
|
||||
|
||||
it('should format with organisation and only first name', () => {
|
||||
// Arrange
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
organisationName: 'Acme Corp',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = formatName(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('Acme Corp - John');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Formats a name by combining first name, last name, and organisation name.
|
||||
*
|
||||
* The function follows these formatting rules:
|
||||
* - Names are formatted as "LastName FirstName"
|
||||
* - Organisation name is separated from the personal name with " - "
|
||||
* - Empty or undefined values are filtered out
|
||||
* - If all values are empty/undefined, returns an empty string
|
||||
*
|
||||
* @param params - The name components to format
|
||||
* @param params.firstName - The person's first name (optional)
|
||||
* @param params.lastName - The person's last name (optional)
|
||||
* @param params.organisationName - The organisation name (optional)
|
||||
* @returns The formatted name string
|
||||
*
|
||||
* @example
|
||||
* formatName({ firstName: 'John', lastName: 'Doe' })
|
||||
* // Returns: "Doe John"
|
||||
*
|
||||
* @example
|
||||
* formatName({ firstName: 'John', lastName: 'Doe', organisationName: 'Acme Corp' })
|
||||
* // Returns: "Acme Corp - Doe John"
|
||||
*
|
||||
* @example
|
||||
* formatName({ organisationName: 'Acme Corp' })
|
||||
* // Returns: "Acme Corp"
|
||||
*/
|
||||
export const formatName = ({
|
||||
firstName,
|
||||
lastName,
|
||||
organisationName,
|
||||
}: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
organisationName?: string;
|
||||
}): string => {
|
||||
const nameCombined = [lastName, firstName].filter((f) => !!f);
|
||||
const organisation = [organisationName].filter((f) => !!f);
|
||||
|
||||
return (
|
||||
[organisation.join(), nameCombined.join(' ')]
|
||||
.filter((f) => !!f)
|
||||
.join(' - ') ?? ''
|
||||
);
|
||||
};
|
||||
13
libs/utils/format-name/src/test-setup.ts
Normal file
13
libs/utils/format-name/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/utils/format-name/tsconfig.json
Normal file
30
libs/utils/format-name/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/utils/format-name/tsconfig.lib.json
Normal file
27
libs/utils/format-name/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/utils/format-name/tsconfig.spec.json
Normal file
29
libs/utils/format-name/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
27
libs/utils/format-name/vite.config.mts
Normal file
27
libs/utils/format-name/vite.config.mts
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/utils/format-name',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/utils/format-name',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user