feat(reward): implement reward catalog with customer selection

Add comprehensive reward catalog functionality including:
- New reward catalog component with header switching between start card and customer card
- Customer selection integration with tab metadata service
- Reward checkout service with query settings fetching
- Customer search integration for reward context with proper filtering
- Tab metadata support for storing selected customer IDs
- Navigation improvements for reward workflow in customer details

The implementation includes proper error handling, logging, and follows
the established architectural patterns with facades and services.

Ref: #5262
This commit is contained in:
Nino
2025-09-15 17:42:57 +02:00
parent 0269473a18
commit e5c09c030c
26 changed files with 396 additions and 129 deletions

View File

@@ -1,7 +1,9 @@
<shared-loader [loading]="fetching$ | async" background="true" spinnerSize="32">
<div class="overflow-scroll max-h-[calc(100vh-15rem)]">
<div class="customer-details-header grid grid-flow-row pb-6">
<div class="customer-details-header-actions flex flex-row justify-end pt-4 px-4">
<div
class="customer-details-header-actions flex flex-row justify-end pt-4 px-4"
>
<page-customer-menu
[customerId]="customerId$ | async"
[processId]="processId$ | async"
@@ -16,8 +18,12 @@
<p>Sind Ihre Kundendaten korrekt?</p>
</div>
</div>
<div class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content h-14">
<div class="pl-4 font-bold grid grid-flow-col justify-start items-center gap-2">
<div
class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content h-14"
>
<div
class="pl-4 font-bold grid grid-flow-col justify-start items-center gap-2"
>
<shared-icon [icon]="customerType$ | async"></shared-icon>
<span>
{{ customerType$ | async }}
@@ -30,19 +36,22 @@
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label font-bold text-brand"
>
>
Bearbeiten
</a>
}
}
</div>
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
<div
class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3"
>
<div class="flex flex-row">
<div class="data-label">Erstellungsdatum</div>
@if (created$ | async; as created) {
<div class="data-value">
{{ created | date: 'dd.MM.yyyy' }} | {{ created | date: 'HH:mm' }} Uhr
{{ created | date: 'dd.MM.yyyy' }} |
{{ created | date: 'HH:mm' }} Uhr
</div>
}
</div>
@@ -136,7 +145,9 @@
@if (!(isBusinessKonto$ | async)) {
<div class="customer-details-customer-main-row">
<div class="data-label">Geburtstag</div>
<div class="data-value">{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}</div>
<div class="data-value">
{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}
</div>
</div>
}
@if (!(isBusinessKonto$ | async) && (organisationName$ | async)) {
@@ -162,24 +173,41 @@
</div>
</shared-loader>
@if (shoppingCartHasNoItems$ | async) {
<button
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
@if (!isRewardTab()) {
@if (shoppingCartHasNoItems$ | async) {
<button
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zur Artikelsuche</shared-loader>
</button>
}
<shared-loader [loading]="showLoader$ | async" spinnerSize="32"
>Weiter zur Artikelsuche</shared-loader
>
</button>
}
@if (shoppingCartHasItems$ | async) {
@if (shoppingCartHasItems$ | async) {
<button
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32"
>Weiter zum Warenkorb</shared-loader
>
</button>
}
} @else {
<button
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
class="w-60 text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="!(hasKundenkarte$ | async)"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32"
>Auswählen</shared-loader
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zum Warenkorb</shared-loader>
</button>
}

View File

@@ -1,4 +1,11 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
inject,
linkedSignal,
} from '@angular/core';
import { Subject, combineLatest } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
import { CustomerSearchNavigation } from '@shared/services/navigation';
@@ -19,12 +26,18 @@ import {
import { UiModalService } from '@ui/modal';
import { ComponentStore } from '@ngrx/component-store';
import { ApplicationService } from '@core/application';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services/navigation';
import {
CheckoutNavigationService,
ProductCatalogNavigationService,
} from '@shared/services/navigation';
import { ActivatedRoute, Router } from '@angular/router';
import { log, logAsync } from '@utils/common';
import { CrmCustomerService } from '@domain/crm';
import { MessageModalComponent, MessageModalData } from '@modal/message';
import { GenderSettingsService } from '@shared/services/gender';
import { injectTab } from '@isa/core/tabs';
import { toSignal } from '@angular/core/rxjs-interop';
import { SelectedCustomerFacade } from '@isa/crm/data-access';
export interface CustomerDetailsViewMainState {
isBusy: boolean;
@@ -57,62 +70,105 @@ export class CustomerDetailsViewMainComponent
private _onDestroy$ = new Subject<void>();
customerService = inject(CrmCustomerService);
selectCustomerFacade = inject(SelectedCustomerFacade);
tab = injectTab();
fetching$ = combineLatest([this._store.fetchingCustomer$, this._store.fetchingCustomerList$]).pipe(
fetching$ = combineLatest([
this._store.fetchingCustomer$,
this._store.fetchingCustomerList$,
]).pipe(
map(([fetchingCustomer, fetchingList]) => fetchingCustomer || fetchingList),
);
isBusy$ = this.select((s) => s.isBusy);
showLoader$ = combineLatest([this.fetching$, this.isBusy$]).pipe(map(([fetching, isBusy]) => fetching || isBusy));
showLoader$ = combineLatest([this.fetching$, this.isBusy$]).pipe(
map(([fetching, isBusy]) => fetching || isBusy),
);
processId$ = this._store.processId$;
processIdSignal = toSignal(this.processId$);
customerId$ = this._store.customerId$;
historyRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.historyRoute({ processId, customerId })),
map(([processId, customerId]) =>
this._navigation.historyRoute({ processId, customerId }),
),
);
ordersRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
map(([processId, customerId]) => this._navigation.ordersRoute({ processId, customerId })),
map(([processId, customerId]) =>
this._navigation.ordersRoute({ processId, customerId }),
),
);
isB2b$ = this._store.isBusinessKonto$;
editRoute$ = combineLatest([this.processId$, this.customerId$, this.isB2b$]).pipe(
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b })),
);
showEditButton$ = combineLatest([this._store.isOnlinekonto$, this._store.isBestellungOhneKonto$]).pipe(
map(([isOnlinekonto, isBestellungOhneKonto]) => !isOnlinekonto && !isBestellungOhneKonto),
);
hasKundenkarte$ = combineLatest([this._store.isKundenkarte$, this._store.isOnlineKontoMitKundenkarte$]).pipe(
map(([isKundenkarte, isOnlineKontoMitKundenkarte]) => isKundenkarte || isOnlineKontoMitKundenkarte),
);
kundenkarteRoute$ = combineLatest([this.hasKundenkarte$, this.processId$, this.customerId$]).pipe(
map(([hasKundenkarte, processId, customerId]) =>
hasKundenkarte ? this._navigation.kundenkarteRoute({ processId, customerId }) : undefined,
editRoute$ = combineLatest([
this.processId$,
this.customerId$,
this.isB2b$,
]).pipe(
map(([processId, customerId, isB2b]) =>
this._navigation.editRoute({ processId, customerId, isB2b }),
),
);
customerType$ = this._store.select((s) => s.customer?.features?.find((f) => f.enabled)?.description);
showEditButton$ = combineLatest([
this._store.isOnlinekonto$,
this._store.isBestellungOhneKonto$,
]).pipe(
map(
([isOnlinekonto, isBestellungOhneKonto]) =>
!isOnlinekonto && !isBestellungOhneKonto,
),
);
hasKundenkarte$ = combineLatest([
this._store.isKundenkarte$,
this._store.isOnlineKontoMitKundenkarte$,
]).pipe(
map(
([isKundenkarte, isOnlineKontoMitKundenkarte]) =>
isKundenkarte || isOnlineKontoMitKundenkarte,
),
);
kundenkarteRoute$ = combineLatest([
this.hasKundenkarte$,
this.processId$,
this.customerId$,
]).pipe(
map(([hasKundenkarte, processId, customerId]) =>
hasKundenkarte
? this._navigation.kundenkarteRoute({ processId, customerId })
: undefined,
),
);
customerType$ = this._store.select(
(s) => s.customer?.features?.find((f) => f.enabled)?.description,
);
created$ = this._store.select((s) => s.customer?.created);
customerNumber$ = this._store.select((s) => s.customer?.customerNumber);
customerNumberDig$ = this._store.select(
(s) => s.customer?.linkedRecords?.find((r) => r.repository === 'dig')?.number,
(s) =>
s.customer?.linkedRecords?.find((r) => r.repository === 'dig')?.number,
);
customerNumberBeeline$ = this._store.select(
(s) => s.customer?.linkedRecords?.find((r) => r.repository === 'beeline')?.number,
(s) =>
s.customer?.linkedRecords?.find((r) => r.repository === 'beeline')
?.number,
);
gender$ = this._store.select((s) => this._genderSettings.getGenderByValue(s.customer?.gender));
gender$ = this._store.select((s) =>
this._genderSettings.getGenderByValue(s.customer?.gender),
);
title$ = this._store.select((s) => s.customer?.title);
@@ -134,7 +190,9 @@ export class CustomerDetailsViewMainComponent
info$ = this._store.select((s) => s.customer?.address?.info);
landline$ = this._store.select((s) => s.customer?.communicationDetails?.phone);
landline$ = this._store.select(
(s) => s.customer?.communicationDetails?.phone,
);
mobile$ = this._store.select((s) => s.customer?.communicationDetails?.mobile);
@@ -148,7 +206,9 @@ export class CustomerDetailsViewMainComponent
shoppingCartHasItems$ = this.select((s) => s.shoppingCart?.items?.length > 0);
shoppingCartHasNoItems$ = this.shoppingCartHasItems$.pipe(map((hasItems) => !hasItems));
shoppingCartHasNoItems$ = this.shoppingCartHasItems$.pipe(
map((hasItems) => !hasItems),
);
dateOfBirth$ = this._store.select((s) => s.customer?.dateOfBirth);
@@ -199,8 +259,12 @@ export class CustomerDetailsViewMainComponent
firstName: customer.firstName,
lastName: customer.lastName,
dateOfBirth: customer.dateOfBirth,
communicationDetails: customer.communicationDetails ? { ...customer.communicationDetails } : undefined,
organisation: customer.organisation ? { ...customer.organisation } : undefined,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
@@ -213,12 +277,18 @@ export class CustomerDetailsViewMainComponent
return this._activatedRoute.snapshot;
}
isOnlineOrCustomerCardUser$ = combineLatest([this.customerType$, this.hasKundenkarte$]).pipe(
map(([type, hasCard]) => type === 'webshop' || hasCard),
);
isOnlineOrCustomerCardUser$ = combineLatest([
this.customerType$,
this.hasKundenkarte$,
]).pipe(map(([type, hasCard]) => type === 'webshop' || hasCard));
constructor() {
super({ isBusy: false, shoppingCart: undefined, shippingAddress: undefined, payer: undefined });
super({
isBusy: false,
shoppingCart: undefined,
shippingAddress: undefined,
payer: undefined,
});
}
setIsBusy(isBusy: boolean) {
@@ -235,6 +305,16 @@ export class CustomerDetailsViewMainComponent
this.patchState({ payer });
}
// TODO: Adjust Logic after new TabService Adapter PR is merged
isRewardTab = linkedSignal(() => {
const tab = this.tab();
const processId = this.processIdSignal();
if (processId === tab?.id) {
return tab?.metadata?.['context'] === 'reward';
}
return false;
});
ngOnInit() {
this.processId$
.pipe(
@@ -254,16 +334,18 @@ export class CustomerDetailsViewMainComponent
// Dies geschieht bereits in der customer-search.component.ts immer wenn eine Navigation ausgeführt wird (sprich für Fälle in denen ein neuer Kunde gesetzt wird).
// Falls aber nach exakt dem gleichen Kunden gesucht wird, muss man diesen hier manuell erneut setzen, ansonsten bleibt dieser im Store auf undefined.
// Da dies nur für die Detailansicht relevant ist, wird hier auch auf hits 1 überprüft, ansonsten befindet man sich sowieso in der Trefferliste.
this._store.customerListResponse$.pipe(takeUntil(this._onDestroy$)).subscribe(([result]) => {
if (result.hits === 1) {
const customerId = result?.result[0].id;
const selectedCustomerId = +this.snapshot.params.customerId;
this._store.customerListResponse$
.pipe(takeUntil(this._onDestroy$))
.subscribe(([result]) => {
if (result.hits === 1) {
const customerId = result?.result[0].id;
const selectedCustomerId = +this.snapshot.params.customerId;
if (customerId === selectedCustomerId) {
this._store.selectCustomer({ customerId });
if (customerId === selectedCustomerId) {
this._store.selectCustomer({ customerId });
}
}
}
});
});
}
ngOnDestroy() {
@@ -316,19 +398,31 @@ export class CustomerDetailsViewMainComponent
this._setShippingAddress();
if (this.shoppingCartHasItems) {
// Navigation zum Warenkorb
const path = this._checkoutNavigation.getCheckoutReviewPath(this.processId).path;
this._router.navigate(path);
if (this.isRewardTab()) {
this.selectCustomerFacade.set(this.processId, this.customer.id);
await this._router.navigate(['/', this.processId, 'reward']);
} else {
// Navigation zur Artikelsuche
const path = this._catalogNavigation.getArticleSearchBasePath(this.processId).path;
this._router.navigate(path);
if (this.shoppingCartHasItems) {
// Navigation zum Warenkorb
const path = this._checkoutNavigation.getCheckoutReviewPath(
this.processId,
).path;
this._router.navigate(path);
} else {
// Navigation zur Artikelsuche
const path = this._catalogNavigation.getArticleSearchBasePath(
this.processId,
).path;
this._router.navigate(path);
}
}
try {
} catch (error) {
this._modalService.error('Warenkorb kann dem Kunden nicht zugewiesen werden', error);
this._modalService.error(
'Warenkorb kann dem Kunden nicht zugewiesen werden',
error,
);
} finally {
this.setIsBusy(false);
}
@@ -336,7 +430,10 @@ export class CustomerDetailsViewMainComponent
@logAsync
_getCurrentBuyer() {
return this._checkoutService.getBuyer({ processId: this.processId }).pipe(first()).toPromise();
return this._checkoutService
.getBuyer({ processId: this.processId })
.pipe(first())
.toPromise();
}
@logAsync
@@ -352,7 +449,9 @@ export class CustomerDetailsViewMainComponent
return true;
}
const required = await this.customerService.canUpgrade(this.customer.id).toPromise();
const required = await this.customerService
.canUpgrade(this.customer.id)
.toPromise();
const upgradeableTo = res.create;
const data: CantAddCustomerToCartData = {
@@ -407,7 +506,10 @@ export class CustomerDetailsViewMainComponent
processId: this.processId,
customerId: this.customer.id,
});
this._router.navigate(nav.path, { queryParams: nav.queryParams, queryParamsHandling: 'merge' });
this._router.navigate(nav.path, {
queryParams: nav.queryParams,
queryParamsHandling: 'merge',
});
},
},
],
@@ -490,7 +592,9 @@ export class CustomerDetailsViewMainComponent
async _updateNotifcationChannelsAsync(currentBuyer: BuyerDTO | undefined) {
if (currentBuyer?.buyerNumber !== this.customer.customerNumber) {
const notificationChannels =
this.customer.notificationChannels === (3 as NotificationChannel) ? 1 : this.customer.notificationChannels;
this.customer.notificationChannels === (3 as NotificationChannel)
? 1
: this.customer.notificationChannels;
this._checkoutService.setNotificationChannels({
processId: this.processId,

View File

@@ -1,8 +1,12 @@
import { Injectable, inject } from '@angular/core';
import { logger } from '@isa/core/logging';
import { SearchService } from '@generated/swagger/cat-search-api';
import {
ResponseArgsOfUISettingsDTO,
SearchService,
} from '@generated/swagger/cat-search-api';
import { firstValueFrom } from 'rxjs';
import { QuerySettings } from '../models';
import { catchResponseArgsErrorPipe } from '@isa/common/data-access';
@Injectable({ providedIn: 'root' })
export class RewardCheckoutService {
@@ -12,7 +16,9 @@ export class RewardCheckoutService {
async fetchQuerySettings(): Promise<QuerySettings> {
this.#logger.info('Fetching query settings from API');
const req$ = this.#searchService.SearchLoyaltySettings();
const req$ = this.#searchService
.SearchLoyaltySettings()
.pipe(catchResponseArgsErrorPipe<ResponseArgsOfUISettingsDTO>());
const res = await firstValueFrom(req$);

View File

@@ -1,4 +1,4 @@
<reward-start-card></reward-start-card>
<reward-header></reward-header>
<!-- <filter-controls-panel (triggerSearch)="search($event)"></filter-controls-panel> -->

View File

@@ -16,11 +16,11 @@ import {
} from '@isa/shared/filter';
import { IconButtonComponent } from '@isa/ui/buttons';
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
import { RewardStartCardComponent } from './reward-start-card/reward-start-card.component';
import { logger } from '@isa/core/logging';
import { createRewardCatalogResource } from './resources';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import { RewardCatalogItemComponent } from './reward-catalog-item/reward-catalog-item.component';
import { RewardHeaderComponent } from './reward-header/reward-header.component';
/**
* Factory function to retrieve query settings from the activated route data.
@@ -45,7 +45,7 @@ function querySettingsFactory() {
FilterControlsPanelComponent,
IconButtonComponent,
EmptyStateComponent,
RewardStartCardComponent,
RewardHeaderComponent,
RewardCatalogItemComponent,
],
host: {

View File

@@ -0,0 +1,3 @@
:host {
@apply w-full flex flex-row gap-20 rounded-2xl bg-isa-neutral-400 p-6 justify-between;
}

View File

@@ -0,0 +1,13 @@
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-1">
<span>NAME {{ selectedCustomerId() }}</span>
<span>LESEPUNKTE</span>
</div>
<button (click)="resetCustomer()" linkButton>Zurücksetzen</button>
</div>
<div class="flex flex-col gap-2">
<span>Prämien ausgewählt</span>
<span>XXX</span>
</div>

View File

@@ -0,0 +1,30 @@
import {
ChangeDetectionStrategy,
Component,
linkedSignal,
inject,
} from '@angular/core';
import { ButtonComponent } from '@isa/ui/buttons';
import { RouterLink } from '@angular/router';
import { SelectedCustomerFacade } from '@isa/crm/data-access';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'reward-customer-card',
templateUrl: './reward-customer-card.component.html',
styleUrl: './reward-customer-card.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent, RouterLink],
})
export class RewardCustomerCardComponent {
tabId = injectTabId();
selectedCustomerFacade = inject(SelectedCustomerFacade);
selectedCustomerId = linkedSignal(() =>
this.selectedCustomerFacade.get(this.tabId()!),
);
resetCustomer() {
this.selectedCustomerFacade.clear(this.tabId()!);
}
}

View File

@@ -0,0 +1,5 @@
@if (selectedCustomerId()) {
<reward-customer-card></reward-customer-card>
} @else {
<reward-start-card></reward-start-card>
}

View File

@@ -0,0 +1,34 @@
import {
ChangeDetectionStrategy,
Component,
inject,
linkedSignal,
} from '@angular/core';
import { RewardStartCardComponent } from './reward-start-card/reward-start-card.component';
import { injectTabId } from '@isa/core/tabs';
import { SelectedCustomerFacade } from '@isa/crm/data-access';
import { RewardCustomerCardComponent } from './reward-customer-card/reward-customer-card.component';
@Component({
selector: 'reward-header',
templateUrl: './reward-header.component.html',
styleUrl: './reward-header.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RewardStartCardComponent, RewardCustomerCardComponent],
})
export class RewardHeaderComponent {
tabId = injectTabId();
selectedCustomerFacade = inject(SelectedCustomerFacade);
selectedCustomerId = linkedSignal(() =>
this.selectedCustomerFacade.get(this.tabId()!),
);
// TODO überprüfen ob customer valide ist, sprich:
// P4M CHECK:
// return !!selectCustomer(s)?.features?.some((c) => c.key === 'p4mUser');
// KUNDENKARTE:
// key
// :
// "d-account" DA MUSS DIE DESCRIPTION "kundenkarte" haben UND die muss enabled: true sein
}

View File

@@ -7,14 +7,16 @@
</p>
</div>
<button
<a
class="reward-start-card__select-cta"
data-which="select-customer"
data-what="select-customer"
uiButton
color="tertiary"
size="large"
(click)="selectCustomer()"
[routerLink]="pathToCrmSearch()"
[queryParams]="queryParams"
(click)="setTabContext()"
>
Kund*in auswählen
</button>
</a>

View File

@@ -0,0 +1,40 @@
import {
ChangeDetectionStrategy,
Component,
linkedSignal,
inject,
} from '@angular/core';
import { ButtonComponent } from '@isa/ui/buttons';
import { injectTabId, TabService } from '@isa/core/tabs';
import { RouterLink } from '@angular/router';
@Component({
selector: 'reward-start-card',
templateUrl: './reward-start-card.component.html',
styleUrl: './reward-start-card.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent, RouterLink],
})
export class RewardStartCardComponent {
tabService = inject(TabService);
tabId = injectTabId();
pathToCrmSearch = linkedSignal(() =>
[
'/kunde',
this.tabId(),
'customer',
{ outlets: { primary: 'search', side: 'search-customer-main' } },
].filter(Boolean),
);
queryParams = {
filter_customertype: 'webshop&loyalty;loyalty&!webshop', // Filter only Customer Card Customers
};
// Wichtig damit Kundensuche weiß, dass wir im Reward Kontext sind
setTabContext() {
this.tabService.patchTab(this.tabId()!, {
metadata: { context: 'reward' },
});
}
}

View File

@@ -1,15 +0,0 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'reward-start-card',
templateUrl: './reward-start-card.component.html',
styleUrl: './reward-start-card.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent],
})
export class RewardStartCardComponent {
selectCustomer() {
console.log('Kunde auswählen');
}
}

View File

@@ -8,4 +8,5 @@ export const AddTabSchema = z.object({
export const PatchTabSchema = z.object({
name: z.string().nonempty().optional(),
tags: z.array(z.string()).optional(),
metadata: z.record(z.any()).optional(),
});

View File

@@ -1 +1,2 @@
export * from './lib/crm-data-access/crm-data-access.component';
export * from './lib/facades';
export * from './lib/constants';

View File

@@ -0,0 +1 @@
export const SELECTED_CUSTOMER_ID = 'selectedCustomerId';

View File

@@ -1 +0,0 @@
<p>CrmDataAccess works!</p>

View File

@@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CrmDataAccessComponent } from './crm-data-access.component';
describe('CrmDataAccessComponent', () => {
let component: CrmDataAccessComponent;
let fixture: ComponentFixture<CrmDataAccessComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CrmDataAccessComponent],
}).compileComponents();
fixture = TestBed.createComponent(CrmDataAccessComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,10 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'crm-crm-data-access',
imports: [CommonModule],
templateUrl: './crm-data-access.component.html',
styleUrl: './crm-data-access.component.css',
})
export class CrmDataAccessComponent {}

View File

@@ -0,0 +1 @@
export * from './selected-customer.facade';

View File

@@ -0,0 +1,19 @@
import { inject, Injectable } from '@angular/core';
import { CrmTabMetadataService } from '../services';
@Injectable({ providedIn: 'root' })
export class SelectedCustomerFacade {
crmTabMetadataService = inject(CrmTabMetadataService);
set(tabId: number, customerId: number) {
this.crmTabMetadataService.setSelectedCustomerId(tabId, customerId);
}
get(tab: number) {
return this.crmTabMetadataService.selectedCustomerId(tab);
}
clear(tab: number) {
this.crmTabMetadataService.setSelectedCustomerId(tab, undefined);
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable, inject } from '@angular/core';
import { TabService } from '@isa/core/tabs';
import { SELECTED_CUSTOMER_ID } from '../constants';
@Injectable({ providedIn: 'root' })
export class CrmTabMetadataService {
#tabService = inject(TabService);
selectedCustomerId(tabId: number): number | undefined {
return this.#metadata(tabId)?.[SELECTED_CUSTOMER_ID] as number;
}
setSelectedCustomerId(tabId: number, customerId: number | undefined) {
this.#tabService.patchTab(tabId, {
metadata: {
[SELECTED_CUSTOMER_ID]: customerId,
},
});
}
#metadata(tabId: number) {
return this.#tabService.entities().find((tab) => tab.id === tabId)
?.metadata;
}
}

View File

@@ -0,0 +1 @@
export * from './crm-tab-metadata.service';