mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2031: feat(crm): add customer bon redemption feature
feat(crm): add customer bon redemption feature - New library @isa/crm/feature/customer-bon-redemption - Implement bon validation and redemption flow - Add SignalStore for state management - Add resource pattern for reactive data loading - Add facade for business logic abstraction - Add Zod schemas for runtime validation - Integrate with loyalty card API endpoints - Add accessibility and E2E test attributes - Remove mock provider (use real facade) - Exclude generated swagger files from linting Components: - BonInputFieldComponent - input with validation - BonDetailsDisplayComponent - shows validated bon - BonRedemptionButtonComponent - redemption action Data Access: - CustomerBonRedemptionFacade - business logic - CustomerBonCheckResource - reactive validation - BonRedemptionStore - component state - CrmSearchService - API integration (checkBon, addBon) Issue: 5314 Related work items: #5314
This commit is contained in:
committed by
Nino Righi
parent
8c0de558a4
commit
fc6d29d62f
@@ -11,10 +11,17 @@
|
|||||||
class="mt-4"
|
class="mt-4"
|
||||||
/>
|
/>
|
||||||
@let cardCode = firstActiveCardCode();
|
@let cardCode = firstActiveCardCode();
|
||||||
|
|
||||||
@if (cardCode) {
|
@if (cardCode) {
|
||||||
|
<crm-customer-bon-redemption
|
||||||
|
[cardCode]="cardCode"
|
||||||
|
class="mt-4"
|
||||||
|
(redeemed)="reloadCardTransactions()"
|
||||||
|
/>
|
||||||
<crm-customer-booking [cardCode]="cardCode" class="mt-4" />
|
<crm-customer-booking [cardCode]="cardCode" class="mt-4" />
|
||||||
|
<crm-customer-card-transactions [cardCode]="cardCode" class="mt-8" />
|
||||||
}
|
}
|
||||||
<crm-customer-card-transactions [cardCode]="cardCode" class="mt-8" />
|
|
||||||
<utils-scroll-top-button
|
<utils-scroll-top-button
|
||||||
[target]="hostElement"
|
[target]="hostElement"
|
||||||
class="flex flex-col justify-self-end fixed bottom-6 right-6"
|
class="flex flex-col justify-self-end fixed bottom-6 right-6"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
effect,
|
effect,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CustomerSearchStore } from '../store';
|
import { CustomerSearchStore } from '../store';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
@@ -14,8 +15,12 @@ import { CustomerMenuComponent } from '../../components/customer-menu';
|
|||||||
import { CustomerLoyaltyCardsComponent } from '@isa/crm/feature/customer-loyalty-cards';
|
import { CustomerLoyaltyCardsComponent } from '@isa/crm/feature/customer-loyalty-cards';
|
||||||
import { CrmFeatureCustomerCardTransactionsComponent } from '@isa/crm/feature/customer-card-transactions';
|
import { CrmFeatureCustomerCardTransactionsComponent } from '@isa/crm/feature/customer-card-transactions';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { CustomerBonusCardsResource } from '@isa/crm/data-access';
|
import {
|
||||||
|
CustomerBonusCardsResource,
|
||||||
|
CustomerCardTransactionsResource,
|
||||||
|
} from '@isa/crm/data-access';
|
||||||
import { CrmFeatureCustomerBookingComponent } from '@isa/crm/feature/customer-booking';
|
import { CrmFeatureCustomerBookingComponent } from '@isa/crm/feature/customer-booking';
|
||||||
|
import { CrmFeatureCustomerBonRedemptionComponent } from '@isa/crm/feature/customer-bon-redemption';
|
||||||
import { ScrollTopButtonComponent } from '@isa/utils/scroll-position';
|
import { ScrollTopButtonComponent } from '@isa/utils/scroll-position';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -30,15 +35,18 @@ import { ScrollTopButtonComponent } from '@isa/utils/scroll-position';
|
|||||||
CustomerLoyaltyCardsComponent,
|
CustomerLoyaltyCardsComponent,
|
||||||
CrmFeatureCustomerCardTransactionsComponent,
|
CrmFeatureCustomerCardTransactionsComponent,
|
||||||
CrmFeatureCustomerBookingComponent,
|
CrmFeatureCustomerBookingComponent,
|
||||||
|
CrmFeatureCustomerBonRedemptionComponent,
|
||||||
ScrollTopButtonComponent,
|
ScrollTopButtonComponent,
|
||||||
],
|
],
|
||||||
providers: [CustomerBonusCardsResource],
|
providers: [CustomerBonusCardsResource, CustomerCardTransactionsResource],
|
||||||
})
|
})
|
||||||
export class KundenkarteMainViewComponent {
|
export class KundenkarteMainViewComponent implements OnDestroy {
|
||||||
|
#reloadTimeoutId?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
private _store = inject(CustomerSearchStore);
|
private _store = inject(CustomerSearchStore);
|
||||||
private _activatedRoute = inject(ActivatedRoute);
|
private _activatedRoute = inject(ActivatedRoute);
|
||||||
private _bonusCardsResource = inject(CustomerBonusCardsResource);
|
private _bonusCardsResource = inject(CustomerBonusCardsResource);
|
||||||
|
#cardTransactionsResource = inject(CustomerCardTransactionsResource);
|
||||||
elementRef = inject(ElementRef);
|
elementRef = inject(ElementRef);
|
||||||
|
|
||||||
get hostElement() {
|
get hostElement() {
|
||||||
@@ -74,4 +82,16 @@ export class KundenkarteMainViewComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadCardTransactions() {
|
||||||
|
this.#reloadTimeoutId = setTimeout(() => {
|
||||||
|
this.#cardTransactionsResource.resource.reload();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.#reloadTimeoutId) {
|
||||||
|
clearTimeout(this.#reloadTimeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module.exports = [
|
|||||||
'**/dist',
|
'**/dist',
|
||||||
'**/vite.config.*.timestamp*',
|
'**/vite.config.*.timestamp*',
|
||||||
'**/vitest.config.*.timestamp*',
|
'**/vitest.config.*.timestamp*',
|
||||||
|
'**/generated/**',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export { KeyValueDTOOfStringAndInteger } from './models/key-value-dtoof-string-a
|
|||||||
export { ResponseArgsOfKeyValueDTOOfStringAndString } from './models/response-args-of-key-value-dtoof-string-and-string';
|
export { ResponseArgsOfKeyValueDTOOfStringAndString } from './models/response-args-of-key-value-dtoof-string-and-string';
|
||||||
export { ResponseArgsOfLoyaltyBookingInfoDTO } from './models/response-args-of-loyalty-booking-info-dto';
|
export { ResponseArgsOfLoyaltyBookingInfoDTO } from './models/response-args-of-loyalty-booking-info-dto';
|
||||||
export { LoyaltyBookingValues } from './models/loyalty-booking-values';
|
export { LoyaltyBookingValues } from './models/loyalty-booking-values';
|
||||||
|
export { ResponseArgsOfLoyaltyBonResponse } from './models/response-args-of-loyalty-bon-response';
|
||||||
|
export { LoyaltyBonResponse } from './models/loyalty-bon-response';
|
||||||
export { LoyaltyBonValues } from './models/loyalty-bon-values';
|
export { LoyaltyBonValues } from './models/loyalty-bon-values';
|
||||||
export { ResponseArgsOfPayerDTO } from './models/response-args-of-payer-dto';
|
export { ResponseArgsOfPayerDTO } from './models/response-args-of-payer-dto';
|
||||||
export { ResponseArgsOfShippingAddressDTO } from './models/response-args-of-shipping-address-dto';
|
export { ResponseArgsOfShippingAddressDTO } from './models/response-args-of-shipping-address-dto';
|
||||||
|
|||||||
13
generated/swagger/crm-api/src/models/loyalty-bon-response.ts
Normal file
13
generated/swagger/crm-api/src/models/loyalty-bon-response.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
export interface LoyaltyBonResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bon Datum
|
||||||
|
*/
|
||||||
|
date?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summe
|
||||||
|
*/
|
||||||
|
total?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
import { ResponseArgs } from './response-args';
|
||||||
|
import { LoyaltyBonResponse } from './loyalty-bon-response';
|
||||||
|
export interface ResponseArgsOfLoyaltyBonResponse extends ResponseArgs{
|
||||||
|
result?: LoyaltyBonResponse;
|
||||||
|
}
|
||||||
@@ -12,8 +12,9 @@ import { ResponseArgsOfLoyaltyBookingInfoDTO } from '../models/response-args-of-
|
|||||||
import { LoyaltyBookingValues } from '../models/loyalty-booking-values';
|
import { LoyaltyBookingValues } from '../models/loyalty-booking-values';
|
||||||
import { ResponseArgsOfIEnumerableOfKeyValueDTOOfStringAndInteger } from '../models/response-args-of-ienumerable-of-key-value-dtoof-string-and-integer';
|
import { ResponseArgsOfIEnumerableOfKeyValueDTOOfStringAndInteger } from '../models/response-args-of-ienumerable-of-key-value-dtoof-string-and-integer';
|
||||||
import { ResponseArgsOfKeyValueDTOOfStringAndString } from '../models/response-args-of-key-value-dtoof-string-and-string';
|
import { ResponseArgsOfKeyValueDTOOfStringAndString } from '../models/response-args-of-key-value-dtoof-string-and-string';
|
||||||
import { ResponseArgsOfBoolean } from '../models/response-args-of-boolean';
|
import { ResponseArgsOfLoyaltyBonResponse } from '../models/response-args-of-loyalty-bon-response';
|
||||||
import { LoyaltyBonValues } from '../models/loyalty-bon-values';
|
import { LoyaltyBonValues } from '../models/loyalty-bon-values';
|
||||||
|
import { ResponseArgsOfBoolean } from '../models/response-args-of-boolean';
|
||||||
import { ResponseArgsOfNullableBoolean } from '../models/response-args-of-nullable-boolean';
|
import { ResponseArgsOfNullableBoolean } from '../models/response-args-of-nullable-boolean';
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -206,7 +207,7 @@ class LoyaltyCardService extends __BaseService {
|
|||||||
*
|
*
|
||||||
* - `locale`:
|
* - `locale`:
|
||||||
*/
|
*/
|
||||||
LoyaltyCardLoyaltyBonCheckResponse(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable<__StrictHttpResponse<ResponseArgsOfBoolean>> {
|
LoyaltyCardLoyaltyBonCheckResponse(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable<__StrictHttpResponse<ResponseArgsOfLoyaltyBonResponse>> {
|
||||||
let __params = this.newParams();
|
let __params = this.newParams();
|
||||||
let __headers = new HttpHeaders();
|
let __headers = new HttpHeaders();
|
||||||
let __body: any = null;
|
let __body: any = null;
|
||||||
@@ -226,7 +227,7 @@ class LoyaltyCardService extends __BaseService {
|
|||||||
return this.http.request<any>(req).pipe(
|
return this.http.request<any>(req).pipe(
|
||||||
__filter(_r => _r instanceof HttpResponse),
|
__filter(_r => _r instanceof HttpResponse),
|
||||||
__map((_r) => {
|
__map((_r) => {
|
||||||
return _r as __StrictHttpResponse<ResponseArgsOfBoolean>;
|
return _r as __StrictHttpResponse<ResponseArgsOfLoyaltyBonResponse>;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -240,9 +241,9 @@ class LoyaltyCardService extends __BaseService {
|
|||||||
*
|
*
|
||||||
* - `locale`:
|
* - `locale`:
|
||||||
*/
|
*/
|
||||||
LoyaltyCardLoyaltyBonCheck(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable<ResponseArgsOfBoolean> {
|
LoyaltyCardLoyaltyBonCheck(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable<ResponseArgsOfLoyaltyBonResponse> {
|
||||||
return this.LoyaltyCardLoyaltyBonCheckResponse(params).pipe(
|
return this.LoyaltyCardLoyaltyBonCheckResponse(params).pipe(
|
||||||
__map(_r => _r.body as ResponseArgsOfBoolean)
|
__map(_r => _r.body as ResponseArgsOfLoyaltyBonResponse)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './lib/resources';
|
|||||||
export * from './lib/helpers';
|
export * from './lib/helpers';
|
||||||
export * from './lib/schemas';
|
export * from './lib/schemas';
|
||||||
export * from './lib/services';
|
export * from './lib/services';
|
||||||
|
export * from './lib/stores';
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ResponseArgs } from '@isa/common/data-access';
|
||||||
|
import { LoyaltyBonResponse } from '@generated/swagger/crm-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock implementation of CustomerBonRedemptionFacade for UI testing without backend.
|
||||||
|
*
|
||||||
|
* Test Bon numbers:
|
||||||
|
* - "123456789" - Valid Bon with data
|
||||||
|
* - "987654321" - Valid Bon with different data
|
||||||
|
* - "111111111" - Valid Bon with high total
|
||||||
|
* - Any other number - Returns "Keine verpunktung möglich"
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // In component for testing, provide the mock:
|
||||||
|
* providers: [
|
||||||
|
* { provide: CustomerBonRedemptionFacade, useClass: CustomerBonRedemptionFacadeMock }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CustomerBonRedemptionFacadeMock {
|
||||||
|
/**
|
||||||
|
* Mock Bon data for testing
|
||||||
|
*/
|
||||||
|
private readonly mockBonData: Record<string, LoyaltyBonResponse> = {
|
||||||
|
'123456789': {
|
||||||
|
date: '23.05.2025',
|
||||||
|
total: 76.12,
|
||||||
|
},
|
||||||
|
'987654321': {
|
||||||
|
date: '15.04.2025',
|
||||||
|
total: 42.5,
|
||||||
|
},
|
||||||
|
'111111111': {
|
||||||
|
date: '01.01.2025',
|
||||||
|
total: 150.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate API delay (300ms)
|
||||||
|
*/
|
||||||
|
private async simulateDelay(): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock check Bon implementation
|
||||||
|
*/
|
||||||
|
async checkBon(params: {
|
||||||
|
cardCode: string;
|
||||||
|
bonNr: string;
|
||||||
|
storeId?: string;
|
||||||
|
}): Promise<ResponseArgs<LoyaltyBonResponse>> {
|
||||||
|
await this.simulateDelay();
|
||||||
|
|
||||||
|
const bonData = this.mockBonData[params.bonNr];
|
||||||
|
|
||||||
|
if (bonData) {
|
||||||
|
return {
|
||||||
|
result: bonData,
|
||||||
|
error: false,
|
||||||
|
message: 'Bon gefunden',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return empty result for unknown Bon numbers (triggers "Keine verpunktung möglich")
|
||||||
|
return {
|
||||||
|
result: undefined,
|
||||||
|
error: false,
|
||||||
|
message: 'Keine verpunktung möglich',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock add Bon implementation
|
||||||
|
*/
|
||||||
|
async addBon(_params: {
|
||||||
|
cardCode: string;
|
||||||
|
bonNr: string;
|
||||||
|
storeId?: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
await this.simulateDelay();
|
||||||
|
|
||||||
|
// Always succeed for mock
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { CrmSearchService } from '../services/crm-search.service';
|
||||||
|
import { CheckBonInput, AddBonInput } from '../schemas';
|
||||||
|
import { LoyaltyBonResponse } from '@generated/swagger/crm-api';
|
||||||
|
import { ResponseArgs } from '@isa/common/data-access';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facade for customer Bon redemption operations.
|
||||||
|
*
|
||||||
|
* Provides a simplified API for validating and redeeming customer receipts (Bons)
|
||||||
|
* for loyalty points.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class CustomerBonRedemptionFacade {
|
||||||
|
#crmSearchService = inject(CrmSearchService);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check/validate a Bon number
|
||||||
|
*
|
||||||
|
* @param params - Bon validation parameters
|
||||||
|
* @param abortSignal - Optional abort signal for cancellation
|
||||||
|
* @returns Response with Bon details (date, total)
|
||||||
|
*/
|
||||||
|
async checkBon(
|
||||||
|
params: CheckBonInput,
|
||||||
|
abortSignal?: AbortSignal,
|
||||||
|
): Promise<ResponseArgs<LoyaltyBonResponse>> {
|
||||||
|
// Fetch store ID if not provided
|
||||||
|
if (!params.storeId) {
|
||||||
|
const store =
|
||||||
|
await this.#crmSearchService.fetchCurrentBookingPartnerStore();
|
||||||
|
params = { ...params, storeId: store?.key };
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#crmSearchService.checkBon(params, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redeem/add a Bon for customer points
|
||||||
|
*
|
||||||
|
* @param params - Bon redemption parameters
|
||||||
|
* @returns True if redemption successful, false otherwise
|
||||||
|
*/
|
||||||
|
async addBon(params: AddBonInput): Promise<boolean> {
|
||||||
|
// Fetch store ID if not provided
|
||||||
|
if (!params.storeId) {
|
||||||
|
const store =
|
||||||
|
await this.#crmSearchService.fetchCurrentBookingPartnerStore();
|
||||||
|
params = { ...params, storeId: store?.key };
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#crmSearchService.addBon(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './customer-cards.facade';
|
export * from './customer-cards.facade';
|
||||||
export * from './customer.facade';
|
export * from './customer.facade';
|
||||||
export * from './customer-card-booking.facade';
|
export * from './customer-card-booking.facade';
|
||||||
|
export * from './customer-bon-redemption.facade';
|
||||||
|
export * from './customer-bon-redemption.facade.mock';
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable, inject, resource, signal, computed } from '@angular/core';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
|
import { CustomerBonRedemptionFacade } from '../facades/customer-bon-redemption.facade';
|
||||||
|
import { LoyaltyBonResponse } from '@generated/swagger/crm-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource for checking/validating Bon numbers.
|
||||||
|
*
|
||||||
|
* Provides reactive loading of Bon details for validation.
|
||||||
|
* Parameters can be updated via `params()` method to trigger validation.
|
||||||
|
*
|
||||||
|
* **Note:** Provide at component level, not root.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CustomerBonCheckResource {
|
||||||
|
readonly #bonFacade = inject(CustomerBonRedemptionFacade);
|
||||||
|
readonly #logger = logger(() => ({ context: 'CustomerBonCheckResource' }));
|
||||||
|
|
||||||
|
readonly #cardCode = signal<string | undefined>(undefined);
|
||||||
|
readonly #bonNr = signal<string | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource that validates Bon based on current parameters.
|
||||||
|
*/
|
||||||
|
readonly resource = resource({
|
||||||
|
params: computed(() => ({
|
||||||
|
cardCode: this.#cardCode(),
|
||||||
|
bonNr: this.#bonNr(),
|
||||||
|
})),
|
||||||
|
loader: async ({
|
||||||
|
params,
|
||||||
|
abortSignal,
|
||||||
|
}): Promise<LoyaltyBonResponse | undefined> => {
|
||||||
|
const { cardCode, bonNr } = params;
|
||||||
|
|
||||||
|
if (!cardCode || !bonNr) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.debug('Checking Bon', () => ({ cardCode, bonNr }));
|
||||||
|
|
||||||
|
const response = await this.#bonFacade.checkBon(
|
||||||
|
{ cardCode, bonNr },
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#logger.debug('Bon checked', () => ({
|
||||||
|
bonNr,
|
||||||
|
found: !!response?.result,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return response?.result;
|
||||||
|
},
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update parameters to trigger Bon validation.
|
||||||
|
*/
|
||||||
|
params(params: { cardCode: string; bonNr: string }): void {
|
||||||
|
this.#cardCode.set(params.cardCode);
|
||||||
|
this.#bonNr.set(params.bonNr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the resource state.
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.#cardCode.set(undefined);
|
||||||
|
this.#bonNr.set(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import { LoyaltyBookingInfoDTO } from '@generated/swagger/crm-api';
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable()
|
||||||
export class CustomerCardTransactionsResource {
|
export class CustomerCardTransactionsResource {
|
||||||
readonly #crmSearchService = inject(CrmSearchService);
|
readonly #crmSearchService = inject(CrmSearchService);
|
||||||
readonly #logger = logger(() => ({
|
readonly #logger = logger(() => ({
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export * from './customer-shipping-addresses.resource';
|
|||||||
export * from './customer.resource';
|
export * from './customer.resource';
|
||||||
export * from './payer.resource';
|
export * from './payer.resource';
|
||||||
export * from './customer-booking-reasons.resource';
|
export * from './customer-booking-reasons.resource';
|
||||||
|
export * from './customer-bon-check.resource';
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for checking/validating a Bon
|
||||||
|
*/
|
||||||
|
export const CheckBonSchema = z.object({
|
||||||
|
cardCode: z.string().min(1, 'Karten-Code ist erforderlich'),
|
||||||
|
bonNr: z.string().min(1, 'Bon-Nummer ist erforderlich'),
|
||||||
|
storeId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CheckBon = z.infer<typeof CheckBonSchema>;
|
||||||
|
export type CheckBonInput = z.input<typeof CheckBonSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for redeeming a Bon
|
||||||
|
*/
|
||||||
|
export const AddBonSchema = z.object({
|
||||||
|
cardCode: z.string().min(1, 'Karten-Code ist erforderlich'),
|
||||||
|
bonNr: z.string().min(1, 'Bon-Nummer ist erforderlich'),
|
||||||
|
storeId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddBon = z.infer<typeof AddBonSchema>;
|
||||||
|
export type AddBonInput = z.input<typeof AddBonSchema>;
|
||||||
@@ -18,3 +18,4 @@ export * from './payment-settings.schema';
|
|||||||
export * from './shipping-address.schema';
|
export * from './shipping-address.schema';
|
||||||
export * from './user.schema';
|
export * from './user.schema';
|
||||||
export * from './add-booking.schema';
|
export * from './add-booking.schema';
|
||||||
|
export * from './bon-redemption.schema';
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import {
|
|||||||
LoyaltyBookingInfoDTO,
|
LoyaltyBookingInfoDTO,
|
||||||
KeyValueDTOOfStringAndString,
|
KeyValueDTOOfStringAndString,
|
||||||
KeyValueDTOOfStringAndInteger,
|
KeyValueDTOOfStringAndInteger,
|
||||||
|
LoyaltyBonResponse,
|
||||||
} from '@generated/swagger/crm-api';
|
} from '@generated/swagger/crm-api';
|
||||||
import {
|
import {
|
||||||
AddBooking,
|
|
||||||
AddBookingInput,
|
AddBookingInput,
|
||||||
AddBookingSchema,
|
AddBookingSchema,
|
||||||
|
CheckBonInput,
|
||||||
|
CheckBonSchema,
|
||||||
|
AddBonInput,
|
||||||
|
AddBonSchema,
|
||||||
Customer,
|
Customer,
|
||||||
FetchCustomerCardsInput,
|
FetchCustomerCardsInput,
|
||||||
FetchCustomerCardsSchema,
|
FetchCustomerCardsSchema,
|
||||||
@@ -22,6 +26,7 @@ import {
|
|||||||
ResponseArgsError,
|
ResponseArgsError,
|
||||||
takeUntilAborted,
|
takeUntilAborted,
|
||||||
} from '@isa/common/data-access';
|
} from '@isa/common/data-access';
|
||||||
|
import { Cache, CacheTimeToLive } from '@isa/common/decorators';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { BonusCardInfo } from '../models';
|
import { BonusCardInfo } from '../models';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
@@ -135,6 +140,7 @@ export class CrmSearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cache({ ttl: CacheTimeToLive.oneHour })
|
||||||
async fetchCurrentBookingPartnerStore(
|
async fetchCurrentBookingPartnerStore(
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<KeyValueDTOOfStringAndString | undefined> {
|
): Promise<KeyValueDTOOfStringAndString | undefined> {
|
||||||
@@ -183,4 +189,66 @@ export class CrmSearchService {
|
|||||||
|
|
||||||
return res?.result;
|
return res?.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check/validate a Bon number
|
||||||
|
*/
|
||||||
|
async checkBon(
|
||||||
|
params: CheckBonInput,
|
||||||
|
abortSignal?: AbortSignal,
|
||||||
|
): Promise<ResponseArgs<LoyaltyBonResponse>> {
|
||||||
|
this.#logger.info('Checking Bon from API');
|
||||||
|
const { cardCode, bonNr, storeId } = CheckBonSchema.parse(params);
|
||||||
|
|
||||||
|
let req$ = this.#loyaltyCardService
|
||||||
|
.LoyaltyCardLoyaltyBonCheck({
|
||||||
|
cardCode,
|
||||||
|
payload: { bonNr, storeId },
|
||||||
|
})
|
||||||
|
.pipe(catchResponseArgsErrorPipe());
|
||||||
|
|
||||||
|
if (abortSignal) {
|
||||||
|
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await firstValueFrom(req$);
|
||||||
|
this.#logger.debug('Successfully checked Bon');
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
const err = new ResponseArgsError(res);
|
||||||
|
this.#logger.error('Bon check failed', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res as ResponseArgs<LoyaltyBonResponse>;
|
||||||
|
} catch (error) {
|
||||||
|
this.#logger.error('Error checking Bon', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redeem/add a Bon for customer points
|
||||||
|
*/
|
||||||
|
async addBon(params: AddBonInput): Promise<boolean> {
|
||||||
|
this.#logger.info('Redeeming Bon from API');
|
||||||
|
const { cardCode, bonNr, storeId } = AddBonSchema.parse(params);
|
||||||
|
|
||||||
|
const req$ = this.#loyaltyCardService.LoyaltyCardLoyaltyBonAdd({
|
||||||
|
cardCode,
|
||||||
|
payload: { bonNr, storeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await firstValueFrom(req$);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
const err = new ResponseArgsError(res);
|
||||||
|
this.#logger.error('Bon redemption failed', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.debug('Successfully redeemed Bon');
|
||||||
|
return res?.result ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
169
libs/crm/data-access/src/lib/stores/bon-redemption.store.ts
Normal file
169
libs/crm/data-access/src/lib/stores/bon-redemption.store.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { computed } from '@angular/core';
|
||||||
|
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
|
||||||
|
import { patchState } from '@ngrx/signals';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validated Bon data structure
|
||||||
|
*
|
||||||
|
* Maps to LoyaltyBonResponse from @generated/swagger/crm-api
|
||||||
|
*/
|
||||||
|
export interface ValidatedBon {
|
||||||
|
bonNumber: string;
|
||||||
|
date: string;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for Bon redemption feature
|
||||||
|
*/
|
||||||
|
interface BonRedemptionState {
|
||||||
|
/**
|
||||||
|
* Current Bon number input by user
|
||||||
|
*/
|
||||||
|
bonNumber: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Bon validation is in progress
|
||||||
|
*/
|
||||||
|
isValidating: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Bon redemption is in progress
|
||||||
|
*/
|
||||||
|
isRedeeming: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks if validation was attempted (button clicked)
|
||||||
|
*/
|
||||||
|
validationAttempted: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validated Bon data after successful validation
|
||||||
|
*/
|
||||||
|
validatedBon: ValidatedBon | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error message from validation or redemption
|
||||||
|
*/
|
||||||
|
errorMessage: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: BonRedemptionState = {
|
||||||
|
bonNumber: '',
|
||||||
|
isValidating: false,
|
||||||
|
isRedeeming: false,
|
||||||
|
validationAttempted: false,
|
||||||
|
validatedBon: undefined,
|
||||||
|
errorMessage: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SignalStore for managing Bon redemption state.
|
||||||
|
*
|
||||||
|
* Component-scoped store (provided in component providers).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Component({
|
||||||
|
* providers: [BonRedemptionStore]
|
||||||
|
* })
|
||||||
|
* export class CrmFeatureCustomerBonRedemptionComponent {
|
||||||
|
* store = inject(BonRedemptionStore);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const BonRedemptionStore = signalStore(
|
||||||
|
withState(initialState),
|
||||||
|
|
||||||
|
withComputed((store) => ({
|
||||||
|
/**
|
||||||
|
* Whether the search button should be disabled
|
||||||
|
*/
|
||||||
|
disableSearch: computed(
|
||||||
|
() =>
|
||||||
|
store.isValidating() ||
|
||||||
|
store.isRedeeming() ||
|
||||||
|
!store.bonNumber().trim(),
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the redemption button should be disabled
|
||||||
|
*/
|
||||||
|
disableRedemption: computed(() => {
|
||||||
|
const bon = store.validatedBon();
|
||||||
|
return store.isRedeeming() || store.isValidating() || !bon;
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a valid Bon is currently loaded
|
||||||
|
*/
|
||||||
|
hasValidBon: computed(() => {
|
||||||
|
const bon = store.validatedBon();
|
||||||
|
return !!bon;
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether there is an error message
|
||||||
|
*/
|
||||||
|
hasError: computed(() => {
|
||||||
|
const error = store.errorMessage();
|
||||||
|
return !!error;
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
|
||||||
|
withMethods((store) => ({
|
||||||
|
/**
|
||||||
|
* Update the Bon number input
|
||||||
|
*/
|
||||||
|
setBonNumber(bonNumber: string): void {
|
||||||
|
patchState(store, {
|
||||||
|
bonNumber,
|
||||||
|
errorMessage: undefined,
|
||||||
|
validatedBon: undefined,
|
||||||
|
validationAttempted: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set validation loading state
|
||||||
|
*/
|
||||||
|
setValidating(isValidating: boolean): void {
|
||||||
|
patchState(store, { isValidating });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set redemption loading state
|
||||||
|
*/
|
||||||
|
setRedeeming(isRedeeming: boolean): void {
|
||||||
|
patchState(store, { isRedeeming });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark that validation was attempted
|
||||||
|
*/
|
||||||
|
setValidationAttempted(validationAttempted: boolean): void {
|
||||||
|
patchState(store, { validationAttempted });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set validated Bon data
|
||||||
|
*/
|
||||||
|
setValidatedBon(validatedBon: ValidatedBon | undefined): void {
|
||||||
|
patchState(store, { validatedBon, errorMessage: undefined });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set error message
|
||||||
|
*/
|
||||||
|
setError(errorMessage: string | undefined): void {
|
||||||
|
patchState(store, { errorMessage, validatedBon: undefined });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset store to initial state
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
patchState(store, initialState);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
1
libs/crm/data-access/src/lib/stores/index.ts
Normal file
1
libs/crm/data-access/src/lib/stores/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './bon-redemption.store';
|
||||||
7
libs/crm/feature/customer-bon-redemption/README.md
Normal file
7
libs/crm/feature/customer-bon-redemption/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# crm-feature-customer-bon-redemption
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test crm-feature-customer-bon-redemption` to execute the unit tests.
|
||||||
34
libs/crm/feature/customer-bon-redemption/eslint.config.cjs
Normal file
34
libs/crm/feature/customer-bon-redemption/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: 'crm',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'crm',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
20
libs/crm/feature/customer-bon-redemption/project.json
Normal file
20
libs/crm/feature/customer-bon-redemption/project.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "crm-feature-customer-bon-redemption",
|
||||||
|
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/crm/feature/customer-bon-redemption/src",
|
||||||
|
"prefix": "crm",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/vite:test",
|
||||||
|
"outputs": ["{options.reportsDirectory}"],
|
||||||
|
"options": {
|
||||||
|
"reportsDirectory": "../../../../coverage/libs/crm/feature/customer-bon-redemption"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/crm/feature/customer-bon-redemption/src/index.ts
Normal file
1
libs/crm/feature/customer-bon-redemption/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component';
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!-- Validated Bon Details -->
|
||||||
|
@if (store.validatedBon(); as bon) {
|
||||||
|
<div
|
||||||
|
class="mb-4 max-w-56"
|
||||||
|
data-what="bon-details"
|
||||||
|
[attr.data-which]="bon.bonNumber"
|
||||||
|
role="region"
|
||||||
|
aria-label="Bon Details"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center py-1">
|
||||||
|
<span class="isa-text-body-2-regular text-isa-neutral-600">Bon Datum</span>
|
||||||
|
<span
|
||||||
|
class="isa-text-body-2-bold text-isa-black"
|
||||||
|
data-what="bon-date"
|
||||||
|
[attr.data-which]="bon.bonNumber"
|
||||||
|
>
|
||||||
|
{{ bon.date }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center py-1">
|
||||||
|
<span class="isa-text-body-2-regular text-isa-neutral-600">Summe</span>
|
||||||
|
<span
|
||||||
|
class="isa-text-body-2-bold text-isa-black"
|
||||||
|
data-what="bon-total"
|
||||||
|
[attr.data-which]="bon.bonNumber"
|
||||||
|
>
|
||||||
|
{{ bon.total | number: '1.2-2' : 'de-DE' }} EUR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
import { DecimalPipe } from '@angular/common';
|
||||||
|
import { BonRedemptionStore } from '@isa/crm/data-access';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart component for displaying validated Bon details.
|
||||||
|
*
|
||||||
|
* Injects BonRedemptionStore to access validated Bon data.
|
||||||
|
*
|
||||||
|
* Shows:
|
||||||
|
* - Bon date
|
||||||
|
* - Total amount
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <crm-bon-details-display />
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'crm-bon-details-display',
|
||||||
|
imports: [DecimalPipe],
|
||||||
|
templateUrl: './bon-details-display.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class BonDetailsDisplayComponent {
|
||||||
|
/**
|
||||||
|
* Store for accessing validated Bon data
|
||||||
|
*/
|
||||||
|
readonly store = inject(BonRedemptionStore);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<!-- Input Section -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<ui-text-field size="large" class="w-full bg-isa-neutral-200">
|
||||||
|
<input
|
||||||
|
class="bg-transparent"
|
||||||
|
#bonInput
|
||||||
|
#bonInputControl="ngModel"
|
||||||
|
type="text"
|
||||||
|
uiInputControl
|
||||||
|
[ngModel]="store.bonNumber()"
|
||||||
|
(ngModelChange)="onBonNumberChange($event)"
|
||||||
|
required
|
||||||
|
placeholder="Bon Nummer / Rechnungs-Nummer (HUG.de)*"
|
||||||
|
data-what="bon-number-input"
|
||||||
|
data-which="customer-loyalty"
|
||||||
|
aria-label="Bon Nummer eingeben"
|
||||||
|
aria-required="true"
|
||||||
|
[attr.aria-invalid]="store.errorMessage() ? 'true' : null"
|
||||||
|
[attr.aria-describedby]="store.errorMessage() ? 'bon-error' : null"
|
||||||
|
(keydown.enter)="onValidate()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (store.bonNumber().trim()) {
|
||||||
|
<ui-text-field-clear (click)="onReset()"></ui-text-field-clear>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
uiTextButton
|
||||||
|
color="strong"
|
||||||
|
size="small"
|
||||||
|
(click)="onValidate()"
|
||||||
|
[pending]="store.isValidating()"
|
||||||
|
[disabled]="bonInputControl.invalid || !cardCode()"
|
||||||
|
data-what="validate-bon-button"
|
||||||
|
data-which="customer-loyalty"
|
||||||
|
aria-label="Bon suchen und validieren"
|
||||||
|
>
|
||||||
|
Bon suchen
|
||||||
|
</button>
|
||||||
|
</ui-text-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success or Error Message -->
|
||||||
|
@if (store.hasValidBon()) {
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 py-2 mb-4 text-isa-accent-green isa-text-body-2-bold"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
data-what="bon-success-message"
|
||||||
|
data-which="customer-loyalty"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">✓</span>
|
||||||
|
<span>Bon gefunden</span>
|
||||||
|
</div>
|
||||||
|
} @else if (store.errorMessage()) {
|
||||||
|
<div
|
||||||
|
id="bon-error"
|
||||||
|
class="py-2 mb-4 text-isa-accent-red isa-text-body-2-bold"
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
data-what="bon-error-message"
|
||||||
|
data-which="customer-loyalty"
|
||||||
|
>
|
||||||
|
{{ store.errorMessage() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
viewChild,
|
||||||
|
ElementRef,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||||
|
import {
|
||||||
|
TextFieldComponent,
|
||||||
|
InputControlDirective,
|
||||||
|
TextFieldClearComponent,
|
||||||
|
} from '@isa/ui/input-controls';
|
||||||
|
import { BonRedemptionStore } from '@isa/crm/data-access';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart component for Bon number input field.
|
||||||
|
*
|
||||||
|
* Injects BonRedemptionStore for state management.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Input field with validation
|
||||||
|
* - Search button (when no result) or clear button (when result/error)
|
||||||
|
* - Success/error message display
|
||||||
|
* - Enter key support for validation
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <crm-bon-input-field
|
||||||
|
* [cardCode]="cardCode"
|
||||||
|
* (validate)="onValidate()"
|
||||||
|
* (clearForm)="onClear()" />
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'crm-bon-input-field',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TextButtonComponent,
|
||||||
|
TextFieldComponent,
|
||||||
|
TextFieldClearComponent,
|
||||||
|
InputControlDirective,
|
||||||
|
],
|
||||||
|
templateUrl: './bon-input-field.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class BonInputFieldComponent {
|
||||||
|
/**
|
||||||
|
* Store for accessing state
|
||||||
|
*/
|
||||||
|
readonly store = inject(BonRedemptionStore);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active card code (required for validation)
|
||||||
|
*/
|
||||||
|
readonly cardCode = input<string | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when validation should be triggered
|
||||||
|
*/
|
||||||
|
readonly validate = output<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when form should be cleared/reset
|
||||||
|
*/
|
||||||
|
readonly clearForm = output<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the input element for focusing
|
||||||
|
*/
|
||||||
|
inputRef = viewChild.required<ElementRef<HTMLInputElement>>('bonInput');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effect to focus input when reset is called
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
// Focus when both error and validBon are false (after reset)
|
||||||
|
if (!this.store.hasError() && !this.store.hasValidBon()) {
|
||||||
|
const inputEl = this.inputRef()?.nativeElement;
|
||||||
|
if (inputEl && document.activeElement !== inputEl) {
|
||||||
|
setTimeout(() => inputEl.focus(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Bon number input changes
|
||||||
|
*/
|
||||||
|
onBonNumberChange(value: string): void {
|
||||||
|
this.store.setBonNumber(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle validation trigger
|
||||||
|
*/
|
||||||
|
onValidate(): void {
|
||||||
|
this.validate.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle reset/clear action
|
||||||
|
*/
|
||||||
|
onReset(): void {
|
||||||
|
this.clearForm.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!-- Redemption Button (Always Visible) -->
|
||||||
|
<button
|
||||||
|
uiButton
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
(click)="onRedeem()"
|
||||||
|
[pending]="store.isRedeeming()"
|
||||||
|
[disabled]="disabled()"
|
||||||
|
data-what="redeem-bon-button"
|
||||||
|
[attr.data-which]="store.validatedBon()?.bonNumber"
|
||||||
|
aria-label="Bon für Kunden verbuchen"
|
||||||
|
>
|
||||||
|
Für Kunden verbuchen
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
inject,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ButtonComponent } from '@isa/ui/buttons';
|
||||||
|
import { BonRedemptionStore } from '@isa/crm/data-access';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart component for Bon redemption action button.
|
||||||
|
*
|
||||||
|
* Injects BonRedemptionStore for state management.
|
||||||
|
*
|
||||||
|
* Displays "Für Kunden verbuchen" button with pending state.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <crm-bon-redemption-button
|
||||||
|
* [disabled]="disabled"
|
||||||
|
* (redeem)="onRedeem()" />
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'crm-bon-redemption-button',
|
||||||
|
imports: [ButtonComponent],
|
||||||
|
templateUrl: './bon-redemption-button.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class BonRedemptionButtonComponent {
|
||||||
|
/**
|
||||||
|
* Store for accessing state
|
||||||
|
*/
|
||||||
|
readonly store = inject(BonRedemptionStore);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the button should be disabled (from parent)
|
||||||
|
*/
|
||||||
|
readonly disabled = input<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when redemption should be triggered
|
||||||
|
*/
|
||||||
|
readonly redeem = output<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle redemption button click
|
||||||
|
*/
|
||||||
|
onRedeem(): void {
|
||||||
|
this.redeem.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/* Scoped element selector for text field */
|
||||||
|
.bon-input-section ui-text-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<div
|
||||||
|
class="bg-isa-neutral-200 rounded-2xl p-6 mt-4"
|
||||||
|
data-what="bon-redemption-container"
|
||||||
|
data-which="customer-loyalty"
|
||||||
|
>
|
||||||
|
<h3 class="isa-text-body-1-bold mb-4">Bon nachträglich bepunkten</h3>
|
||||||
|
|
||||||
|
<!-- Input Field with Search/Clear Button and Validation Messages -->
|
||||||
|
<crm-bon-input-field
|
||||||
|
[cardCode]="cardCode()"
|
||||||
|
(validate)="validateBon()"
|
||||||
|
(clearForm)="resetForm()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Validated Bon Details -->
|
||||||
|
<crm-bon-details-display />
|
||||||
|
|
||||||
|
<!-- Redemption Button (Always Visible) -->
|
||||||
|
<crm-bon-redemption-button
|
||||||
|
[disabled]="disableRedemption()"
|
||||||
|
(redeem)="redeemBon()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CrmFeatureCustomerBonRedemptionComponent } from './crm-feature-customer-bon-redemption.component';
|
||||||
|
|
||||||
|
describe('CrmFeatureCustomerBonRedemptionComponent', () => {
|
||||||
|
let component: CrmFeatureCustomerBonRedemptionComponent;
|
||||||
|
let fixture: ComponentFixture<CrmFeatureCustomerBonRedemptionComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CrmFeatureCustomerBonRedemptionComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CrmFeatureCustomerBonRedemptionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
inject,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
injectFeedbackDialog,
|
||||||
|
injectFeedbackErrorDialog,
|
||||||
|
} from '@isa/ui/dialog';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
|
import {
|
||||||
|
CustomerBonRedemptionFacade,
|
||||||
|
CustomerBonCheckResource,
|
||||||
|
BonRedemptionStore,
|
||||||
|
} from '@isa/crm/data-access';
|
||||||
|
import { ResponseArgsError } from '@isa/common/data-access';
|
||||||
|
import { BonInputFieldComponent } from './components/bon-input-field/bon-input-field.component';
|
||||||
|
import { BonDetailsDisplayComponent } from './components/bon-details-display/bon-details-display.component';
|
||||||
|
import { BonRedemptionButtonComponent } from './components/bon-redemption-button/bon-redemption-button.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for redeeming customer receipts (Bon) for loyalty points.
|
||||||
|
*
|
||||||
|
* Allows users to:
|
||||||
|
* 1. Enter a Bon number
|
||||||
|
* 2. Validate the Bon exists and is valid
|
||||||
|
* 3. View Bon summary (date, total)
|
||||||
|
* 4. Redeem the Bon for customer points
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <crm-customer-bon-redemption [cardCode]="activeCardCode()" />
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'crm-customer-bon-redemption',
|
||||||
|
imports: [
|
||||||
|
BonInputFieldComponent,
|
||||||
|
BonDetailsDisplayComponent,
|
||||||
|
BonRedemptionButtonComponent,
|
||||||
|
],
|
||||||
|
providers: [BonRedemptionStore, CustomerBonCheckResource],
|
||||||
|
templateUrl: './crm-feature-customer-bon-redemption.component.html',
|
||||||
|
styleUrl: './crm-feature-customer-bon-redemption.component.css',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class CrmFeatureCustomerBonRedemptionComponent {
|
||||||
|
#logger = logger(() => ({
|
||||||
|
component: 'CrmFeatureCustomerBonRedemptionComponent',
|
||||||
|
cardCode: this.cardCode(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
#bonCheckResource = inject(CustomerBonCheckResource);
|
||||||
|
#bonFacade = inject(CustomerBonRedemptionFacade);
|
||||||
|
#errorFeedbackDialog = injectFeedbackErrorDialog();
|
||||||
|
#feedbackDialog = injectFeedbackDialog();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store for managing Bon redemption state
|
||||||
|
*/
|
||||||
|
readonly store = inject(BonRedemptionStore);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active loyalty card code for the customer
|
||||||
|
*/
|
||||||
|
readonly cardCode = input<string | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed: Disable redemption if no card code
|
||||||
|
*/
|
||||||
|
readonly disableRedemption = computed(
|
||||||
|
() => this.store.disableRedemption() || !this.cardCode(),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly redeemed = output<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor - sets up effects to sync resource state with store
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
// Sync resource loading state with store
|
||||||
|
effect(() => {
|
||||||
|
const isLoading = this.#bonCheckResource.resource.isLoading();
|
||||||
|
this.store.setValidating(isLoading);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync resource results with store
|
||||||
|
effect(() => {
|
||||||
|
const result = this.#bonCheckResource.resource.value();
|
||||||
|
const error = this.#bonCheckResource.resource.error();
|
||||||
|
const isLoading = this.#bonCheckResource.resource.isLoading();
|
||||||
|
const validationAttempted = this.store.validationAttempted();
|
||||||
|
|
||||||
|
// Only process results if validation was attempted
|
||||||
|
if (!validationAttempted || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle validation result
|
||||||
|
if (result) {
|
||||||
|
this.store.setValidatedBon({
|
||||||
|
bonNumber: this.store.bonNumber(),
|
||||||
|
date: result.date ?? '',
|
||||||
|
total: result.total ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if validation returned no result
|
||||||
|
else if (!error && !result) {
|
||||||
|
this.store.setError('Keine verpunktung möglich');
|
||||||
|
}
|
||||||
|
// Handle API errors
|
||||||
|
else if (error) {
|
||||||
|
let errorMsg = 'Bon-Validierung fehlgeschlagen';
|
||||||
|
if (error instanceof ResponseArgsError) {
|
||||||
|
errorMsg = error.message || errorMsg;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMsg = error.message;
|
||||||
|
}
|
||||||
|
this.store.setError(errorMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the entered Bon number
|
||||||
|
*/
|
||||||
|
validateBon(): void {
|
||||||
|
const cardCode = this.cardCode();
|
||||||
|
const bonNr = this.store.bonNumber().trim();
|
||||||
|
|
||||||
|
if (!cardCode || !bonNr) {
|
||||||
|
this.#logger.warn(
|
||||||
|
'Cannot validate Bon: missing required parameters',
|
||||||
|
() => ({
|
||||||
|
hasCardCode: !!cardCode,
|
||||||
|
hasBonNr: !!bonNr,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.debug('Triggering Bon validation', () => ({
|
||||||
|
cardCode,
|
||||||
|
bonNr,
|
||||||
|
}));
|
||||||
|
this.store.setValidationAttempted(true);
|
||||||
|
this.#bonCheckResource.params({ cardCode, bonNr });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redeem the validated Bon for customer points
|
||||||
|
*/
|
||||||
|
async redeemBon() {
|
||||||
|
this.store.setRedeeming(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cardCode = this.cardCode();
|
||||||
|
const bonNr = this.store.bonNumber().trim();
|
||||||
|
const validatedBon = this.store.validatedBon();
|
||||||
|
|
||||||
|
if (!cardCode) {
|
||||||
|
throw new Error('Kein Karten-Code vorhanden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bonNr || !validatedBon) {
|
||||||
|
throw new Error('Bon muss zuerst validiert werden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.debug('Redeeming Bon', () => ({ cardCode, bonNr }));
|
||||||
|
|
||||||
|
const success = await this.#bonFacade.addBon({ cardCode, bonNr });
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error('Bon-Einlösung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.info('Bon redeemed successfully', () => ({
|
||||||
|
bonNr,
|
||||||
|
total: validatedBon.total,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.#feedbackDialog({
|
||||||
|
data: {
|
||||||
|
message: 'Bon wurde erfolgreich gebucht',
|
||||||
|
autoClose: true,
|
||||||
|
autoCloseDelay: 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
this.resetForm();
|
||||||
|
this.redeemed.emit();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.#logger.error('Bon redemption failed', error as Error, () => ({
|
||||||
|
bonNr: this.store.bonNumber(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let errorMsg = 'Bon-Einlösung fehlgeschlagen';
|
||||||
|
|
||||||
|
if (error instanceof ResponseArgsError) {
|
||||||
|
errorMsg = error.message || errorMsg;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMsg = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#errorFeedbackDialog({
|
||||||
|
data: {
|
||||||
|
errorMessage: errorMsg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.store.setRedeeming(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the form to initial state
|
||||||
|
*/
|
||||||
|
resetForm(): void {
|
||||||
|
this.store.reset();
|
||||||
|
this.#bonCheckResource.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
libs/crm/feature/customer-bon-redemption/src/test-setup.ts
Normal file
13
libs/crm/feature/customer-bon-redemption/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/crm/feature/customer-bon-redemption/tsconfig.json
Normal file
30
libs/crm/feature/customer-bon-redemption/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/crm/feature/customer-bon-redemption/tsconfig.lib.json
Normal file
27
libs/crm/feature/customer-bon-redemption/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/crm/feature/customer-bon-redemption/tsconfig.spec.json
Normal file
29
libs/crm/feature/customer-bon-redemption/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"]
|
||||||
|
}
|
||||||
35
libs/crm/feature/customer-bon-redemption/vite.config.mts
Normal file
35
libs/crm/feature/customer-bon-redemption/vite.config.mts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/// <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
|
||||||
|
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
||||||
|
defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir:
|
||||||
|
'../../../../node_modules/.vite/libs/crm/feature/customer-bon-redemption',
|
||||||
|
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',
|
||||||
|
['junit', { outputFile: '../../../../testresults/junit-crm-feature-customer-bon-redemption.xml' }],
|
||||||
|
],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory:
|
||||||
|
'../../../../coverage/libs/crm/feature/customer-bon-redemption',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
reporter: ['text', 'cobertura'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Cards carousel -->
|
<!-- Cards carousel -->
|
||||||
<crm-customer-cards-carousel [cards]="cardList" class="max-w-[657px]" />
|
<crm-customer-cards-carousel [cards]="cardList" class="w-full" />
|
||||||
|
|
||||||
<!-- Action buttons (TODO: implement) -->
|
<!-- Action buttons (TODO: implement) -->
|
||||||
<div class="flex gap-4" data-what="card-actions">
|
<div class="flex gap-4" data-what="card-actions">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.ui-text-field-clear {
|
.ui-text-field-clear {
|
||||||
ui-icon-button {
|
ui-icon-button {
|
||||||
@apply text-isa-neutral-900;
|
@apply text-isa-neutral-900 bg-transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
computed,
|
computed,
|
||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TextFieldComponent } from './text-field.component';
|
import { TextFieldComponent } from './text-field.component';
|
||||||
import { provideIcons } from '@ng-icons/core';
|
import { provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionClose } from '@isa/icons';
|
import { isaActionClose } from '@isa/icons';
|
||||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ui-text-field-clear',
|
selector: 'ui-text-field-clear',
|
||||||
templateUrl: './text-field-clear.component.html',
|
templateUrl: './text-field-clear.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
host: {
|
host: {
|
||||||
'[class]': '["ui-text-field-clear", sizeClass()]',
|
'[class]': '["ui-text-field-clear", sizeClass()]',
|
||||||
},
|
},
|
||||||
providers: [provideIcons({ isaActionClose })],
|
providers: [provideIcons({ isaActionClose })],
|
||||||
imports: [IconButtonComponent],
|
imports: [IconButtonComponent],
|
||||||
})
|
})
|
||||||
export class TextFieldClearComponent {
|
export class TextFieldClearComponent {
|
||||||
hostComponent = inject(TextFieldComponent, { host: true });
|
hostComponent = inject(TextFieldComponent, { host: true });
|
||||||
|
|
||||||
size = this.hostComponent.size;
|
size = this.hostComponent.size;
|
||||||
|
|
||||||
sizeClass = computed(() => {
|
sizeClass = computed(() => {
|
||||||
return `ui-text-field-clear__${this.size()}`;
|
return `ui-text-field-clear__${this.size()}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
"@isa/core/storage": ["libs/core/storage/src/index.ts"],
|
"@isa/core/storage": ["libs/core/storage/src/index.ts"],
|
||||||
"@isa/core/tabs": ["libs/core/tabs/src/index.ts"],
|
"@isa/core/tabs": ["libs/core/tabs/src/index.ts"],
|
||||||
"@isa/crm/data-access": ["libs/crm/data-access/src/index.ts"],
|
"@isa/crm/data-access": ["libs/crm/data-access/src/index.ts"],
|
||||||
|
"@isa/crm/feature/customer-bon-redemption": [
|
||||||
|
"libs/crm/feature/customer-bon-redemption/src/index.ts"
|
||||||
|
],
|
||||||
"@isa/crm/feature/customer-booking": [
|
"@isa/crm/feature/customer-booking": [
|
||||||
"libs/crm/feature/customer-booking/src/index.ts"
|
"libs/crm/feature/customer-booking/src/index.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user