();
+
+ /**
+ * 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"
],