diff --git a/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.html b/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.html index 9fc039af8..45a8d9a16 100644 --- a/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.html +++ b/apps/isa-app/src/page/customer/customer-search/kundenkarte-main-view/kundenkarte-main-view.component.html @@ -11,10 +11,17 @@ class="mt-4" /> @let cardCode = firstActiveCardCode(); + @if (cardCode) { + + } - + ; + private _store = inject(CustomerSearchStore); private _activatedRoute = inject(ActivatedRoute); private _bonusCardsResource = inject(CustomerBonusCardsResource); - + #cardTransactionsResource = inject(CustomerCardTransactionsResource); elementRef = inject(ElementRef); 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); + } + } } diff --git a/eslint.config.js b/eslint.config.js index 08ad65cc6..e8bb59ab8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,6 +10,7 @@ module.exports = [ '**/dist', '**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*', + '**/generated/**', ], }, // { diff --git a/generated/swagger/crm-api/src/models.ts b/generated/swagger/crm-api/src/models.ts index 0da0bd4dd..60b7c7499 100644 --- a/generated/swagger/crm-api/src/models.ts +++ b/generated/swagger/crm-api/src/models.ts @@ -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 { ResponseArgsOfLoyaltyBookingInfoDTO } from './models/response-args-of-loyalty-booking-info-dto'; 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 { ResponseArgsOfPayerDTO } from './models/response-args-of-payer-dto'; export { ResponseArgsOfShippingAddressDTO } from './models/response-args-of-shipping-address-dto'; diff --git a/generated/swagger/crm-api/src/models/loyalty-bon-response.ts b/generated/swagger/crm-api/src/models/loyalty-bon-response.ts new file mode 100644 index 000000000..dc74a173b --- /dev/null +++ b/generated/swagger/crm-api/src/models/loyalty-bon-response.ts @@ -0,0 +1,13 @@ +/* tslint:disable */ +export interface LoyaltyBonResponse { + + /** + * Bon Datum + */ + date?: string; + + /** + * Summe + */ + total?: number; +} diff --git a/generated/swagger/crm-api/src/models/response-args-of-loyalty-bon-response.ts b/generated/swagger/crm-api/src/models/response-args-of-loyalty-bon-response.ts new file mode 100644 index 000000000..4e2f5d26f --- /dev/null +++ b/generated/swagger/crm-api/src/models/response-args-of-loyalty-bon-response.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +import { ResponseArgs } from './response-args'; +import { LoyaltyBonResponse } from './loyalty-bon-response'; +export interface ResponseArgsOfLoyaltyBonResponse extends ResponseArgs{ + result?: LoyaltyBonResponse; +} diff --git a/generated/swagger/crm-api/src/services/loyalty-card.service.ts b/generated/swagger/crm-api/src/services/loyalty-card.service.ts index 02eba0ec4..d693fe7d9 100644 --- a/generated/swagger/crm-api/src/services/loyalty-card.service.ts +++ b/generated/swagger/crm-api/src/services/loyalty-card.service.ts @@ -12,8 +12,9 @@ import { ResponseArgsOfLoyaltyBookingInfoDTO } from '../models/response-args-of- import { LoyaltyBookingValues } from '../models/loyalty-booking-values'; 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 { 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 { ResponseArgsOfBoolean } from '../models/response-args-of-boolean'; import { ResponseArgsOfNullableBoolean } from '../models/response-args-of-nullable-boolean'; @Injectable({ providedIn: 'root', @@ -206,7 +207,7 @@ class LoyaltyCardService extends __BaseService { * * - `locale`: */ - LoyaltyCardLoyaltyBonCheckResponse(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable<__StrictHttpResponse> { + LoyaltyCardLoyaltyBonCheckResponse(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable<__StrictHttpResponse> { let __params = this.newParams(); let __headers = new HttpHeaders(); let __body: any = null; @@ -226,7 +227,7 @@ class LoyaltyCardService extends __BaseService { return this.http.request(req).pipe( __filter(_r => _r instanceof HttpResponse), __map((_r) => { - return _r as __StrictHttpResponse; + return _r as __StrictHttpResponse; }) ); } @@ -240,9 +241,9 @@ class LoyaltyCardService extends __BaseService { * * - `locale`: */ - LoyaltyCardLoyaltyBonCheck(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable { + LoyaltyCardLoyaltyBonCheck(params: LoyaltyCardService.LoyaltyCardLoyaltyBonCheckParams): __Observable { return this.LoyaltyCardLoyaltyBonCheckResponse(params).pipe( - __map(_r => _r.body as ResponseArgsOfBoolean) + __map(_r => _r.body as ResponseArgsOfLoyaltyBonResponse) ); } diff --git a/libs/crm/data-access/src/index.ts b/libs/crm/data-access/src/index.ts index 48cf23a51..5e7603053 100644 --- a/libs/crm/data-access/src/index.ts +++ b/libs/crm/data-access/src/index.ts @@ -5,3 +5,4 @@ export * from './lib/resources'; export * from './lib/helpers'; export * from './lib/schemas'; export * from './lib/services'; +export * from './lib/stores'; diff --git a/libs/crm/data-access/src/lib/facades/customer-bon-redemption.facade.mock.ts b/libs/crm/data-access/src/lib/facades/customer-bon-redemption.facade.mock.ts new file mode 100644 index 000000000..41ec8ebc1 --- /dev/null +++ b/libs/crm/data-access/src/lib/facades/customer-bon-redemption.facade.mock.ts @@ -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 = { + '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 { + return new Promise((resolve) => setTimeout(resolve, 300)); + } + + /** + * Mock check Bon implementation + */ + async checkBon(params: { + cardCode: string; + bonNr: string; + storeId?: string; + }): Promise> { + 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 { + await this.simulateDelay(); + + // Always succeed for mock + return true; + } +} diff --git a/libs/crm/data-access/src/lib/facades/customer-bon-redemption.facade.ts b/libs/crm/data-access/src/lib/facades/customer-bon-redemption.facade.ts new file mode 100644 index 000000000..78dbed08a --- /dev/null +++ b/libs/crm/data-access/src/lib/facades/customer-bon-redemption.facade.ts @@ -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> { + // 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 { + // 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); + } +} diff --git a/libs/crm/data-access/src/lib/facades/index.ts b/libs/crm/data-access/src/lib/facades/index.ts index be887d595..2820ae160 100644 --- a/libs/crm/data-access/src/lib/facades/index.ts +++ b/libs/crm/data-access/src/lib/facades/index.ts @@ -1,3 +1,5 @@ export * from './customer-cards.facade'; export * from './customer.facade'; export * from './customer-card-booking.facade'; +export * from './customer-bon-redemption.facade'; +export * from './customer-bon-redemption.facade.mock'; diff --git a/libs/crm/data-access/src/lib/resources/customer-bon-check.resource.ts b/libs/crm/data-access/src/lib/resources/customer-bon-check.resource.ts new file mode 100644 index 000000000..d5c4091ec --- /dev/null +++ b/libs/crm/data-access/src/lib/resources/customer-bon-check.resource.ts @@ -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(undefined); + readonly #bonNr = signal(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 => { + 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); + } +} diff --git a/libs/crm/data-access/src/lib/resources/customer-card-transactions.resource.ts b/libs/crm/data-access/src/lib/resources/customer-card-transactions.resource.ts index e7fdb5471..65a40d5a8 100644 --- a/libs/crm/data-access/src/lib/resources/customer-card-transactions.resource.ts +++ b/libs/crm/data-access/src/lib/resources/customer-card-transactions.resource.ts @@ -27,7 +27,7 @@ import { LoyaltyBookingInfoDTO } from '@generated/swagger/crm-api'; * } * ``` */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class CustomerCardTransactionsResource { readonly #crmSearchService = inject(CrmSearchService); readonly #logger = logger(() => ({ diff --git a/libs/crm/data-access/src/lib/resources/index.ts b/libs/crm/data-access/src/lib/resources/index.ts index 8fa1352ba..13bb6db7a 100644 --- a/libs/crm/data-access/src/lib/resources/index.ts +++ b/libs/crm/data-access/src/lib/resources/index.ts @@ -8,3 +8,4 @@ export * from './customer-shipping-addresses.resource'; export * from './customer.resource'; export * from './payer.resource'; export * from './customer-booking-reasons.resource'; +export * from './customer-bon-check.resource'; diff --git a/libs/crm/data-access/src/lib/schemas/bon-redemption.schema.ts b/libs/crm/data-access/src/lib/schemas/bon-redemption.schema.ts new file mode 100644 index 000000000..63253a29d --- /dev/null +++ b/libs/crm/data-access/src/lib/schemas/bon-redemption.schema.ts @@ -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; +export type CheckBonInput = z.input; + +/** + * 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; +export type AddBonInput = z.input; diff --git a/libs/crm/data-access/src/lib/schemas/index.ts b/libs/crm/data-access/src/lib/schemas/index.ts index 5f48835e6..8252c1ab0 100644 --- a/libs/crm/data-access/src/lib/schemas/index.ts +++ b/libs/crm/data-access/src/lib/schemas/index.ts @@ -18,3 +18,4 @@ export * from './payment-settings.schema'; export * from './shipping-address.schema'; export * from './user.schema'; export * from './add-booking.schema'; +export * from './bon-redemption.schema'; diff --git a/libs/crm/data-access/src/lib/services/crm-search.service.ts b/libs/crm/data-access/src/lib/services/crm-search.service.ts index 4e3b3be75..6333ecc31 100644 --- a/libs/crm/data-access/src/lib/services/crm-search.service.ts +++ b/libs/crm/data-access/src/lib/services/crm-search.service.ts @@ -5,11 +5,15 @@ import { LoyaltyBookingInfoDTO, KeyValueDTOOfStringAndString, KeyValueDTOOfStringAndInteger, + LoyaltyBonResponse, } from '@generated/swagger/crm-api'; import { - AddBooking, AddBookingInput, AddBookingSchema, + CheckBonInput, + CheckBonSchema, + AddBonInput, + AddBonSchema, Customer, FetchCustomerCardsInput, FetchCustomerCardsSchema, @@ -22,6 +26,7 @@ import { ResponseArgsError, takeUntilAborted, } from '@isa/common/data-access'; +import { Cache, CacheTimeToLive } from '@isa/common/decorators'; import { firstValueFrom } from 'rxjs'; import { BonusCardInfo } from '../models'; import { logger } from '@isa/core/logging'; @@ -135,6 +140,7 @@ export class CrmSearchService { } } + @Cache({ ttl: CacheTimeToLive.oneHour }) async fetchCurrentBookingPartnerStore( abortSignal?: AbortSignal, ): Promise { @@ -183,4 +189,66 @@ export class CrmSearchService { return res?.result; } + + /** + * Check/validate a Bon number + */ + async checkBon( + params: CheckBonInput, + abortSignal?: AbortSignal, + ): Promise> { + 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; + } catch (error) { + this.#logger.error('Error checking Bon', error); + throw error; + } + } + + /** + * Redeem/add a Bon for customer points + */ + async addBon(params: AddBonInput): Promise { + 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; + } } diff --git a/libs/crm/data-access/src/lib/stores/bon-redemption.store.ts b/libs/crm/data-access/src/lib/stores/bon-redemption.store.ts new file mode 100644 index 000000000..a0a7edcc2 --- /dev/null +++ b/libs/crm/data-access/src/lib/stores/bon-redemption.store.ts @@ -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); + }, + })), +); diff --git a/libs/crm/data-access/src/lib/stores/index.ts b/libs/crm/data-access/src/lib/stores/index.ts new file mode 100644 index 000000000..7d644909a --- /dev/null +++ b/libs/crm/data-access/src/lib/stores/index.ts @@ -0,0 +1 @@ +export * from './bon-redemption.store'; diff --git a/libs/crm/feature/customer-bon-redemption/README.md b/libs/crm/feature/customer-bon-redemption/README.md new file mode 100644 index 000000000..f970d0406 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/README.md @@ -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. diff --git a/libs/crm/feature/customer-bon-redemption/eslint.config.cjs b/libs/crm/feature/customer-bon-redemption/eslint.config.cjs new file mode 100644 index 000000000..36a9a7b6a --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/eslint.config.cjs @@ -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: {}, + }, +]; diff --git a/libs/crm/feature/customer-bon-redemption/project.json b/libs/crm/feature/customer-bon-redemption/project.json new file mode 100644 index 000000000..19a44aeec --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/project.json @@ -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" + } + } +} diff --git a/libs/crm/feature/customer-bon-redemption/src/index.ts b/libs/crm/feature/customer-bon-redemption/src/index.ts new file mode 100644 index 000000000..6692b3c0f --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/index.ts @@ -0,0 +1 @@ +export * from './lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component'; diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-details-display/bon-details-display.component.html b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-details-display/bon-details-display.component.html new file mode 100644 index 000000000..346f5710b --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-details-display/bon-details-display.component.html @@ -0,0 +1,32 @@ + +@if (store.validatedBon(); as bon) { +
+
+ Bon Datum + + {{ bon.date }} + +
+ +
+ Summe + + {{ bon.total | number: '1.2-2' : 'de-DE' }} EUR + +
+
+} diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-details-display/bon-details-display.component.ts b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-details-display/bon-details-display.component.ts new file mode 100644 index 000000000..ae7ff712d --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-details-display/bon-details-display.component.ts @@ -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 + * + */ +@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); +} diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-input-field/bon-input-field.component.html b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-input-field/bon-input-field.component.html new file mode 100644 index 000000000..009dcd46f --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-input-field/bon-input-field.component.html @@ -0,0 +1,65 @@ + +
+ + + + @if (store.bonNumber().trim()) { + + } + + +
+ + +@if (store.hasValidBon()) { +
+ + Bon gefunden +
+} @else if (store.errorMessage()) { + +} diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-input-field/bon-input-field.component.ts b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-input-field/bon-input-field.component.ts new file mode 100644 index 000000000..e65faeef3 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-input-field/bon-input-field.component.ts @@ -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 + * + */ +@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(undefined); + + /** + * Emits when validation should be triggered + */ + readonly validate = output(); + + /** + * Emits when form should be cleared/reset + */ + readonly clearForm = output(); + + /** + * Reference to the input element for focusing + */ + inputRef = viewChild.required>('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(); + } +} diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-redemption-button/bon-redemption-button.component.html b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-redemption-button/bon-redemption-button.component.html new file mode 100644 index 000000000..8ec2645e1 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-redemption-button/bon-redemption-button.component.html @@ -0,0 +1,14 @@ + + diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-redemption-button/bon-redemption-button.component.ts b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-redemption-button/bon-redemption-button.component.ts new file mode 100644 index 000000000..9795a603a --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/components/bon-redemption-button/bon-redemption-button.component.ts @@ -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 + * + */ +@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(false); + + /** + * Emits when redemption should be triggered + */ + readonly redeem = output(); + + /** + * Handle redemption button click + */ + onRedeem(): void { + this.redeem.emit(); + } +} diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.css b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.css new file mode 100644 index 000000000..d0e41669c --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.css @@ -0,0 +1,4 @@ +/* Scoped element selector for text field */ +.bon-input-section ui-text-field { + flex: 1; +} diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.html b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.html new file mode 100644 index 000000000..ba562baf4 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.html @@ -0,0 +1,23 @@ +
+

Bon nachträglich bepunkten

+ + + + + + + + + +
diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.spec.ts b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.spec.ts new file mode 100644 index 000000000..9b0745cec --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CrmFeatureCustomerBonRedemptionComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CrmFeatureCustomerBonRedemptionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.ts b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.ts new file mode 100644 index 000000000..71c9ca292 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/lib/crm-feature-customer-bon-redemption/crm-feature-customer-bon-redemption.component.ts @@ -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 + * + */ +@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(undefined); + + /** + * Computed: Disable redemption if no card code + */ + readonly disableRedemption = computed( + () => this.store.disableRedemption() || !this.cardCode(), + ); + + readonly redeemed = output(); + + /** + * 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(); + } +} diff --git a/libs/crm/feature/customer-bon-redemption/src/test-setup.ts b/libs/crm/feature/customer-bon-redemption/src/test-setup.ts new file mode 100644 index 000000000..cebf5ae72 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/src/test-setup.ts @@ -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(), +); diff --git a/libs/crm/feature/customer-bon-redemption/tsconfig.json b/libs/crm/feature/customer-bon-redemption/tsconfig.json new file mode 100644 index 000000000..06f8b89a6 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/tsconfig.json @@ -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" + } + ] +} diff --git a/libs/crm/feature/customer-bon-redemption/tsconfig.lib.json b/libs/crm/feature/customer-bon-redemption/tsconfig.lib.json new file mode 100644 index 000000000..9259117c2 --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/tsconfig.lib.json @@ -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"] +} diff --git a/libs/crm/feature/customer-bon-redemption/tsconfig.spec.json b/libs/crm/feature/customer-bon-redemption/tsconfig.spec.json new file mode 100644 index 000000000..b2f92f3ec --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/tsconfig.spec.json @@ -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"] +} diff --git a/libs/crm/feature/customer-bon-redemption/vite.config.mts b/libs/crm/feature/customer-bon-redemption/vite.config.mts new file mode 100644 index 000000000..06e8be4ca --- /dev/null +++ b/libs/crm/feature/customer-bon-redemption/vite.config.mts @@ -0,0 +1,35 @@ +/// +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'], + }, + }, +})); diff --git a/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.css b/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.css index e69de29bb..68b397a65 100644 --- a/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.css +++ b/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.css @@ -0,0 +1,3 @@ +:host { + @apply min-w-0; +} diff --git a/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.html b/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.html index 71902195b..6244e25c2 100644 --- a/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.html +++ b/libs/crm/feature/customer-loyalty-cards/src/lib/customer-loyalty-cards.component.html @@ -28,7 +28,7 @@ /> - +
diff --git a/libs/ui/input-controls/src/lib/text-field/_text-field-clear.scss b/libs/ui/input-controls/src/lib/text-field/_text-field-clear.scss index 93f563e09..dbccad7ba 100644 --- a/libs/ui/input-controls/src/lib/text-field/_text-field-clear.scss +++ b/libs/ui/input-controls/src/lib/text-field/_text-field-clear.scss @@ -1,5 +1,5 @@ -.ui-text-field-clear { - ui-icon-button { - @apply text-isa-neutral-900; - } -} +.ui-text-field-clear { + ui-icon-button { + @apply text-isa-neutral-900 bg-transparent; + } +} diff --git a/libs/ui/input-controls/src/lib/text-field/text-field-clear.component.ts b/libs/ui/input-controls/src/lib/text-field/text-field-clear.component.ts index 7af72bb4e..2b59467d6 100644 --- a/libs/ui/input-controls/src/lib/text-field/text-field-clear.component.ts +++ b/libs/ui/input-controls/src/lib/text-field/text-field-clear.component.ts @@ -1,31 +1,31 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - inject, -} from '@angular/core'; -import { TextFieldComponent } from './text-field.component'; -import { provideIcons } from '@ng-icons/core'; -import { isaActionClose } from '@isa/icons'; -import { IconButtonComponent } from '@isa/ui/buttons'; - -@Component({ - selector: 'ui-text-field-clear', - templateUrl: './text-field-clear.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - host: { - '[class]': '["ui-text-field-clear", sizeClass()]', - }, - providers: [provideIcons({ isaActionClose })], - imports: [IconButtonComponent], -}) -export class TextFieldClearComponent { - hostComponent = inject(TextFieldComponent, { host: true }); - - size = this.hostComponent.size; - - sizeClass = computed(() => { - return `ui-text-field-clear__${this.size()}`; - }); -} +import { + ChangeDetectionStrategy, + Component, + computed, + inject, +} from '@angular/core'; +import { TextFieldComponent } from './text-field.component'; +import { provideIcons } from '@ng-icons/core'; +import { isaActionClose } from '@isa/icons'; +import { IconButtonComponent } from '@isa/ui/buttons'; + +@Component({ + selector: 'ui-text-field-clear', + templateUrl: './text-field-clear.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + host: { + '[class]': '["ui-text-field-clear", sizeClass()]', + }, + providers: [provideIcons({ isaActionClose })], + imports: [IconButtonComponent], +}) +export class TextFieldClearComponent { + hostComponent = inject(TextFieldComponent, { host: true }); + + size = this.hostComponent.size; + + sizeClass = computed(() => { + return `ui-text-field-clear__${this.size()}`; + }); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index ce4de57b6..fadbc8479 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -69,6 +69,9 @@ "@isa/core/storage": ["libs/core/storage/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/feature/customer-bon-redemption": [ + "libs/crm/feature/customer-bon-redemption/src/index.ts" + ], "@isa/crm/feature/customer-booking": [ "libs/crm/feature/customer-booking/src/index.ts" ],