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.hasValue() ? this.#bonCheckResource.resource.value() : undefined; 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) { const errorMsg = this.#extractErrorMessage(error); 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(); } /** * Extract error message from various error types. * ResponseArgsError already formats invalidProperties into a readable message. */ #extractErrorMessage(error: unknown): string { const defaultMsg = 'Bon-Validierung fehlgeschlagen'; const actualError = (error as { cause?: unknown })?.cause ?? error; if (actualError instanceof ResponseArgsError) { return actualError.message || defaultMsg; } if (actualError instanceof Error) { return actualError.message || defaultMsg; } return defaultMsg; } }