mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merged PR 2030: feat(crm-customer-booking): add loyalty card booking component
feat(crm-customer-booking): add loyalty card booking component Implement new component for customer loyalty card credit/debit bookings with booking type selection and real-time transaction updates. Includes automatic reload of transaction history after successful bookings. Key changes: - Add CrmFeatureCustomerBookingComponent with booking form UI - Create CustomerCardBookingFacade for booking API calls - Add CustomerBookingReasonsResource for loading booking types - Extend CrmSearchService with booking methods (addBooking, fetchBookingReasons, fetchCurrentBookingPartnerStore) - Add AddBookingSchema with Zod validation - Integrate component into KundenkarteMainViewComponent - Update CustomerCardTransactionsResource to providedIn: 'root' for shared access - Improve transaction list UX (hide header/center empty state when no data) Technical details: - New library: @isa/crm/feature/customer-booking (Vitest-based) - Signals-based state management with computed properties - Automatic points calculation based on booking type multiplier - Error handling with feedback dialogs - 500ms delay before transaction reload to ensure API consistency - Data attributes for E2E testing (data-what, data-which) Ref: #5315
This commit is contained in:
committed by
Lorenz Hilpert
parent
71af23544f
commit
a855e79196
@@ -0,0 +1,31 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { CrmSearchService } from '../services/crm-search.service';
|
||||
import { AddBookingInput } from '../schemas';
|
||||
import {
|
||||
KeyValueDTOOfStringAndInteger,
|
||||
KeyValueDTOOfStringAndString,
|
||||
LoyaltyBookingInfoDTO,
|
||||
} from '@generated/swagger/crm-api';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CustomerCardBookingFacade {
|
||||
#crmSearchService = inject(CrmSearchService);
|
||||
|
||||
async fetchBookingReasons(
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<KeyValueDTOOfStringAndInteger[]> {
|
||||
return this.#crmSearchService.fetchBookingReasons(abortSignal);
|
||||
}
|
||||
|
||||
async fetchCurrentBookingPartnerStore(
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<KeyValueDTOOfStringAndString | undefined> {
|
||||
return this.#crmSearchService.fetchCurrentBookingPartnerStore(abortSignal);
|
||||
}
|
||||
|
||||
async addBooking(
|
||||
params: AddBookingInput,
|
||||
): Promise<LoyaltyBookingInfoDTO | undefined> {
|
||||
return this.#crmSearchService.addBooking(params);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './customer-cards.facade';
|
||||
export * from './customer.facade';
|
||||
export * from './customer-card-booking.facade';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Injectable, inject, resource } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { CrmSearchService } from '@isa/crm/data-access';
|
||||
import { KeyValueDTOOfStringAndInteger } from '@generated/swagger/crm-api';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerBookingReasonsResource {
|
||||
readonly #crmSearchService = inject(CrmSearchService);
|
||||
readonly #logger = logger(() => ({
|
||||
context: 'CustomerBookingReasonsResource',
|
||||
}));
|
||||
|
||||
readonly resource = resource({
|
||||
loader: async ({
|
||||
abortSignal,
|
||||
}): Promise<KeyValueDTOOfStringAndInteger[] | undefined> => {
|
||||
this.#logger.debug('Loading Booking Reasons');
|
||||
|
||||
const reasons =
|
||||
await this.#crmSearchService.fetchBookingReasons(abortSignal);
|
||||
|
||||
this.#logger.debug('Booking Reasons loaded', () => ({
|
||||
count: reasons.length,
|
||||
}));
|
||||
|
||||
return reasons;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -15,9 +15,6 @@ import { LoyaltyBookingInfoDTO } from '@generated/swagger/crm-api';
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* providers: [CustomerCardTransactionsResource],
|
||||
* })
|
||||
* export class MyFeatureComponent {
|
||||
* #transactionsResource = inject(CustomerCardTransactionsResource);
|
||||
*
|
||||
@@ -30,7 +27,7 @@ import { LoyaltyBookingInfoDTO } from '@generated/swagger/crm-api';
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CustomerCardTransactionsResource {
|
||||
readonly #crmSearchService = inject(CrmSearchService);
|
||||
readonly #logger = logger(() => ({
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './customer-shipping-address.resource';
|
||||
export * from './customer-shipping-addresses.resource';
|
||||
export * from './customer.resource';
|
||||
export * from './payer.resource';
|
||||
export * from './customer-booking-reasons.resource';
|
||||
|
||||
18
libs/crm/data-access/src/lib/schemas/add-booking.schema.ts
Normal file
18
libs/crm/data-access/src/lib/schemas/add-booking.schema.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const AddBookingSchema = z.object({
|
||||
cardCode: z.string().describe('Unique card code identifier'),
|
||||
booking: z
|
||||
.object({
|
||||
points: z.number().describe('Booking points'),
|
||||
reason: z.string().optional().describe('Booking Reason'),
|
||||
storeId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Booking store (convercus store id)'),
|
||||
})
|
||||
.describe('Booking details'),
|
||||
});
|
||||
|
||||
export type AddBooking = z.infer<typeof AddBookingSchema>;
|
||||
export type AddBookingInput = z.input<typeof AddBookingSchema>;
|
||||
@@ -17,3 +17,4 @@ export * from './payer.schema';
|
||||
export * from './payment-settings.schema';
|
||||
export * from './shipping-address.schema';
|
||||
export * from './user.schema';
|
||||
export * from './add-booking.schema';
|
||||
|
||||
@@ -3,8 +3,13 @@ import {
|
||||
CustomerService,
|
||||
LoyaltyCardService,
|
||||
LoyaltyBookingInfoDTO,
|
||||
KeyValueDTOOfStringAndString,
|
||||
KeyValueDTOOfStringAndInteger,
|
||||
} from '@generated/swagger/crm-api';
|
||||
import {
|
||||
AddBooking,
|
||||
AddBookingInput,
|
||||
AddBookingSchema,
|
||||
Customer,
|
||||
FetchCustomerCardsInput,
|
||||
FetchCustomerCardsSchema,
|
||||
@@ -14,6 +19,7 @@ import {
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
ResponseArgs,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
@@ -104,4 +110,77 @@ export class CrmSearchService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async fetchBookingReasons(
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<KeyValueDTOOfStringAndInteger[]> {
|
||||
this.#logger.info('Fetching booking reasons from API');
|
||||
|
||||
let req$ = this.#loyaltyCardService
|
||||
.LoyaltyCardBookingReason()
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully fetched booking reasons');
|
||||
|
||||
return res?.result || [];
|
||||
} catch (error) {
|
||||
this.#logger.error('Error fetching booking reasons', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async fetchCurrentBookingPartnerStore(
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<KeyValueDTOOfStringAndString | undefined> {
|
||||
this.#logger.info('Fetching current booking partner store from API');
|
||||
|
||||
let req$ = this.#loyaltyCardService
|
||||
.LoyaltyCardCurrentBookingPartnerStore()
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully fetched current booking partner store');
|
||||
|
||||
return res?.result;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error fetching current booking partner store', error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async addBooking(
|
||||
params: AddBookingInput,
|
||||
): Promise<LoyaltyBookingInfoDTO | undefined> {
|
||||
const parsed = AddBookingSchema.parse(params);
|
||||
|
||||
const req$ = this.#loyaltyCardService.LoyaltyCardAddBooking({
|
||||
cardCode: parsed.cardCode,
|
||||
booking: {
|
||||
points: parsed.booking.points,
|
||||
reason: parsed.booking.reason,
|
||||
storeId: parsed.booking.storeId,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error('Add Booking Failed', err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return res?.result;
|
||||
}
|
||||
}
|
||||
|
||||
7
libs/crm/feature/customer-booking/README.md
Normal file
7
libs/crm/feature/customer-booking/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# crm-feature-customer-booking
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test crm-feature-customer-booking` to execute the unit tests.
|
||||
34
libs/crm/feature/customer-booking/eslint.config.cjs
Normal file
34
libs/crm/feature/customer-booking/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: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/crm/feature/customer-booking/project.json
Normal file
20
libs/crm/feature/customer-booking/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "crm-feature-customer-booking",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/crm/feature/customer-booking/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../../coverage/libs/crm/feature/customer-booking"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/crm/feature/customer-booking/src/index.ts
Normal file
1
libs/crm/feature/customer-booking/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/crm-feature-customer-booking/crm-feature-customer-booking.component';
|
||||
@@ -0,0 +1,15 @@
|
||||
:host {
|
||||
@apply h-[15.5rem] flex flex-col gap-4 rounded-2xl bg-isa-neutral-200 p-8 justify-between;
|
||||
}
|
||||
|
||||
/* Remove number input arrows */
|
||||
input[type='number']::-webkit-outer-spin-button,
|
||||
input[type='number']::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@if (cardCode() && !bookingReasonsLoading()) {
|
||||
<div class="flex flex-col gap-1 text-isa-neutral-900">
|
||||
<span class="isa-text-body-1-bold">Kulanzbuchungen</span>
|
||||
<span class="isa-text-body-2-regular">1€ entspricht 10 Lesepunkten</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-[1fr,auto] items-center justify-between gap-4 border rounded-lg border-isa-neutral-900 px-4 py-1"
|
||||
>
|
||||
<div class="isa-text-body-1-bold">Buchen</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center">
|
||||
@if (selectedReason(); as reason) {
|
||||
<span class="isa-text-body-2-bold text-isa-neutral-900 px-2">
|
||||
{{ reason.value > 0 ? '+' : '-' }}
|
||||
</span>
|
||||
}
|
||||
<input
|
||||
name="points"
|
||||
placeholder="Punkte"
|
||||
type="number"
|
||||
[ngModel]="points()"
|
||||
(ngModelChange)="points.set($event)"
|
||||
min="0"
|
||||
data-what="input"
|
||||
data-which="points"
|
||||
class="w-20 isa-text-body-2-bold bg-isa-neutral-200 placeholder:isa-text-body-2-regular placeholder:text-isa-neutral-500 text-isa-neutral-900 focus:outline-none px-4 text-right border-none"
|
||||
/>
|
||||
</div>
|
||||
<ui-dropdown
|
||||
[ngModel]="selectedReasonKey()"
|
||||
(ngModelChange)="selectedReasonKey.set($event)"
|
||||
class="border-none w-[14rem] truncate"
|
||||
[label]="dropdownLabel()"
|
||||
data-what="dropdown"
|
||||
data-which="booking-reason"
|
||||
>
|
||||
@if (bookingReasons(); as reasons) {
|
||||
@for (reason of reasons; track reason.key) {
|
||||
<ui-dropdown-option
|
||||
[value]="reason.label"
|
||||
data-what="dropdown-option"
|
||||
data-which="reason-option"
|
||||
[attr.data-reason-key]="reason.key"
|
||||
>
|
||||
{{ reason.label }}
|
||||
</ui-dropdown-option>
|
||||
}
|
||||
}
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-40"
|
||||
uiButton
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="booking()"
|
||||
[disabled]="disableBooking()"
|
||||
[pending]="isBooking()"
|
||||
data-what="button"
|
||||
data-which="booking-submit"
|
||||
>
|
||||
Jetzt buchen
|
||||
</button>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CrmFeatureCustomerBookingComponent } from './crm-feature-customer-booking.component';
|
||||
|
||||
describe('CrmFeatureCustomerBookingComponent', () => {
|
||||
let component: CrmFeatureCustomerBookingComponent;
|
||||
let fixture: ComponentFixture<CrmFeatureCustomerBookingComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CrmFeatureCustomerBookingComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CrmFeatureCustomerBookingComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
signal,
|
||||
computed,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
CustomerBookingReasonsResource,
|
||||
CustomerCardBookingFacade,
|
||||
CustomerCardTransactionsResource,
|
||||
} from '@isa/crm/data-access';
|
||||
import {
|
||||
injectFeedbackDialog,
|
||||
injectFeedbackErrorDialog,
|
||||
} from '@isa/ui/dialog';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import {
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
|
||||
@Component({
|
||||
selector: 'crm-customer-booking',
|
||||
imports: [
|
||||
FormsModule,
|
||||
ButtonComponent,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
],
|
||||
templateUrl: './crm-feature-customer-booking.component.html',
|
||||
styleUrl: './crm-feature-customer-booking.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [CustomerBookingReasonsResource],
|
||||
})
|
||||
export class CrmFeatureCustomerBookingComponent {
|
||||
#logger = logger(() => ({
|
||||
component: 'CrmFeatureCustomerBookingComponent',
|
||||
}));
|
||||
#customerCardBookingFacade = inject(CustomerCardBookingFacade);
|
||||
#bookingReasonsResource = inject(CustomerBookingReasonsResource);
|
||||
#transactionResource = inject(CustomerCardTransactionsResource);
|
||||
#errorFeedbackDialog = injectFeedbackErrorDialog();
|
||||
#feedbackDialog = injectFeedbackDialog();
|
||||
readonly cardCode = input<string | undefined>(undefined);
|
||||
|
||||
readonly bookingReasons = this.#bookingReasonsResource.resource.value;
|
||||
readonly bookingReasonsLoading =
|
||||
this.#bookingReasonsResource.resource.isLoading;
|
||||
|
||||
points = signal<number | undefined>(undefined);
|
||||
selectedReasonKey = signal<string | undefined>(undefined);
|
||||
isBooking = signal(false);
|
||||
|
||||
selectedReason = computed(() => {
|
||||
const key = this.selectedReasonKey();
|
||||
const reasons = this.bookingReasons();
|
||||
return reasons?.find((r) => r.key === key);
|
||||
});
|
||||
|
||||
calculatedPoints = computed(() => {
|
||||
const reason = this.selectedReason();
|
||||
const pointsValue = this.points();
|
||||
if (!reason || !pointsValue) return 0;
|
||||
return pointsValue * (reason.value ?? 1);
|
||||
});
|
||||
|
||||
disableBooking = computed(() => {
|
||||
return (
|
||||
this.isBooking() ||
|
||||
this.bookingReasonsLoading() ||
|
||||
!this.selectedReasonKey() ||
|
||||
!this.points() ||
|
||||
this.points() === 0
|
||||
);
|
||||
});
|
||||
|
||||
dropdownLabel = computed(() => {
|
||||
const reason = this.selectedReason()?.label;
|
||||
return reason ?? 'Buchungstyp';
|
||||
});
|
||||
|
||||
async booking() {
|
||||
this.isBooking.set(true);
|
||||
try {
|
||||
const cardCode = this.cardCode();
|
||||
const reason = this.selectedReason();
|
||||
const calculatedPoints = this.calculatedPoints();
|
||||
|
||||
if (!cardCode) {
|
||||
throw new Error('Kein Karten-Code vorhanden');
|
||||
}
|
||||
|
||||
if (!reason) {
|
||||
throw new Error('Kein Buchungsgrund ausgewählt');
|
||||
}
|
||||
|
||||
if (calculatedPoints === 0) {
|
||||
throw new Error('Punktezahl muss größer als 0 sein');
|
||||
}
|
||||
|
||||
const currentBookingPartnerStore =
|
||||
await this.#customerCardBookingFacade.fetchCurrentBookingPartnerStore();
|
||||
const storeId = currentBookingPartnerStore?.key;
|
||||
|
||||
await this.#customerCardBookingFacade.addBooking({
|
||||
cardCode,
|
||||
booking: {
|
||||
points: calculatedPoints,
|
||||
reason: reason.key,
|
||||
storeId: storeId,
|
||||
},
|
||||
});
|
||||
|
||||
this.#feedbackDialog({
|
||||
data: {
|
||||
message: `${reason.label} erfolgreich durchgeführt`,
|
||||
},
|
||||
});
|
||||
this.reloadTransactionHistory();
|
||||
} catch (error: any) {
|
||||
this.#logger.error('Booking Failed', () => ({ error }));
|
||||
this.#errorFeedbackDialog({
|
||||
data: {
|
||||
errorMessage: error?.message ?? 'Buchen/Stornieren fehlgeschlagen',
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.isBooking.set(false);
|
||||
this.resetInputs();
|
||||
}
|
||||
}
|
||||
|
||||
resetInputs() {
|
||||
this.points.set(undefined);
|
||||
this.selectedReasonKey.set(undefined);
|
||||
}
|
||||
|
||||
reloadTransactionHistory() {
|
||||
// Timeout to ensure that the new booking is available in the transaction history
|
||||
setTimeout(() => {
|
||||
this.#transactionResource.params({ cardCode: this.cardCode() });
|
||||
this.#transactionResource.resource.reload();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
13
libs/crm/feature/customer-booking/src/test-setup.ts
Normal file
13
libs/crm/feature/customer-booking/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-booking/tsconfig.json
Normal file
30
libs/crm/feature/customer-booking/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-booking/tsconfig.lib.json
Normal file
27
libs/crm/feature/customer-booking/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-booking/tsconfig.spec.json
Normal file
29
libs/crm/feature/customer-booking/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"]
|
||||
}
|
||||
28
libs/crm/feature/customer-booking/vite.config.mts
Normal file
28
libs/crm/feature/customer-booking/vite.config.mts
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <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 defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../../node_modules/.vite/libs/crm/feature/customer-booking',
|
||||
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'],
|
||||
coverage: {
|
||||
reportsDirectory:
|
||||
'../../../../coverage/libs/crm/feature/customer-booking',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,7 +1,9 @@
|
||||
<div class="flex flex-col gap-[24px] px-4 overflow-hidden overflow-y-scroll">
|
||||
<h2 class="isa-text-body-1-bold text-isa-neutral-900">
|
||||
Letzte Transaktionen des Kunden
|
||||
</h2>
|
||||
<div class="flex flex-col gap-[24px] px-4">
|
||||
@if (transactions()?.length) {
|
||||
<h2 class="isa-text-body-1-bold text-isa-neutral-900">
|
||||
Letzte 5 Transaktionen des Kunden
|
||||
</h2>
|
||||
}
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="text-isa-neutral-500 text-sm">Lade Transaktionen...</div>
|
||||
@@ -11,9 +13,10 @@
|
||||
</div>
|
||||
} @else if (!transactions()?.length) {
|
||||
<ui-empty-state
|
||||
class="self-center"
|
||||
title="Keine Transaktionen"
|
||||
description="Für diese Kundenkarte wurden noch keine Transaktionen erfasst"
|
||||
appearance="no-results"
|
||||
appearance="noResults"
|
||||
/>
|
||||
} @else {
|
||||
<table cdk-table [dataSource]="dataSource()" [trackBy]="trackByDate">
|
||||
|
||||
@@ -24,10 +24,7 @@ import { LoyaltyBookingInfoDTO } from '@generated/swagger/crm-api';
|
||||
NgIconComponent,
|
||||
EmptyStateComponent,
|
||||
],
|
||||
providers: [
|
||||
CustomerCardTransactionsResource,
|
||||
provideIcons({ isaActionPolygonUp, isaActionPolygonDown }),
|
||||
],
|
||||
providers: [provideIcons({ isaActionPolygonUp, isaActionPolygonDown })],
|
||||
templateUrl: './crm-feature-customer-card-transactions.component.html',
|
||||
styleUrl: './crm-feature-customer-card-transactions.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -78,6 +75,8 @@ export class CrmFeatureCustomerCardTransactionsComponent {
|
||||
const code = this.cardCode();
|
||||
this.#logger.debug('Card code changed', () => ({ cardCode: code }));
|
||||
this.#transactionsResource.params({ cardCode: code });
|
||||
|
||||
console.log(this.transactions());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user