mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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$);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<reward-start-card></reward-start-card>
|
||||
<reward-header></reward-header>
|
||||
|
||||
<!-- <filter-controls-panel (triggerSearch)="search($event)"></filter-controls-panel> -->
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply w-full flex flex-row gap-20 rounded-2xl bg-isa-neutral-400 p-6 justify-between;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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()!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@if (selectedCustomerId()) {
|
||||
<reward-customer-card></reward-customer-card>
|
||||
} @else {
|
||||
<reward-start-card></reward-start-card>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './lib/crm-data-access/crm-data-access.component';
|
||||
export * from './lib/facades';
|
||||
export * from './lib/constants';
|
||||
|
||||
1
libs/crm/data-access/src/lib/constants.ts
Normal file
1
libs/crm/data-access/src/lib/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SELECTED_CUSTOMER_ID = 'selectedCustomerId';
|
||||
@@ -1 +0,0 @@
|
||||
<p>CrmDataAccess works!</p>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 {}
|
||||
1
libs/crm/data-access/src/lib/facades/index.ts
Normal file
1
libs/crm/data-access/src/lib/facades/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './selected-customer.facade';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
1
libs/crm/data-access/src/lib/services/index.ts
Normal file
1
libs/crm/data-access/src/lib/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './crm-tab-metadata.service';
|
||||
Reference in New Issue
Block a user