mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2040: fix(crm): prevent duplicate reload of loyalty points
fix(crm): prevent duplicate reload of loyalty points Refactored reload mechanism to use parent-managed pattern: - Child components emit events instead of reloading directly - Parent coordinates reload of both transactions and bonus cards - Added loading guards to prevent concurrent requests - Added JSDoc documentation to public methods Closes #5497 Related work items: #5497
This commit is contained in:
committed by
Nino Righi
parent
644c33ddc3
commit
5f1d3a2c7b
@@ -17,10 +17,18 @@
|
||||
<crm-customer-bon-redemption
|
||||
[cardCode]="cardCode"
|
||||
class="mt-4"
|
||||
(redeemed)="reloadCardTransactions()"
|
||||
(redeemed)="reloadCardTransactionsAndCards()"
|
||||
/>
|
||||
<crm-customer-booking
|
||||
[cardCode]="cardCode"
|
||||
class="mt-4"
|
||||
(booked)="reloadCardTransactionsAndCards()"
|
||||
/>
|
||||
<crm-customer-card-transactions
|
||||
[cardCode]="cardCode"
|
||||
class="mt-8"
|
||||
(reload)="reloadCardTransactionsAndCards()"
|
||||
/>
|
||||
<crm-customer-booking [cardCode]="cardCode" class="mt-4" />
|
||||
<crm-customer-card-transactions [cardCode]="cardCode" class="mt-8" />
|
||||
}
|
||||
|
||||
<utils-scroll-top-button
|
||||
|
||||
@@ -47,13 +47,16 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _bonusCardsResource = inject(CustomerBonusCardsResource);
|
||||
#bonusCardsResource = inject(CustomerBonusCardsResource);
|
||||
#cardTransactionsResource = inject(CustomerCardTransactionsResource);
|
||||
elementRef = inject(ElementRef);
|
||||
#router = inject(Router);
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#customerNavigationService = inject(CustomerSearchNavigation);
|
||||
|
||||
/**
|
||||
* Returns the native DOM element of this component
|
||||
*/
|
||||
get hostElement() {
|
||||
return this.elementRef.nativeElement;
|
||||
}
|
||||
@@ -73,7 +76,7 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
* Get the first active card code
|
||||
*/
|
||||
readonly firstActiveCardCode = computed(() => {
|
||||
const cards = this._bonusCardsResource.resource.value();
|
||||
const cards = this.#bonusCardsResource.resource.value();
|
||||
const firstActiveCard = cards?.find((card) => card.isActive);
|
||||
return firstActiveCard?.code;
|
||||
});
|
||||
@@ -83,14 +86,24 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
effect(() => {
|
||||
const customerId = this.customerId();
|
||||
if (customerId) {
|
||||
this._bonusCardsResource.params({ customerId: Number(customerId) });
|
||||
this.#bonusCardsResource.params({ customerId: Number(customerId) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reloadCardTransactions() {
|
||||
/**
|
||||
* Reloads both card transactions and bonus cards resources after a 500ms delay.
|
||||
* Only triggers reload if the resource is not currently loading to prevent concurrent requests.
|
||||
*/
|
||||
reloadCardTransactionsAndCards() {
|
||||
this.#reloadTimeoutId = setTimeout(() => {
|
||||
this.#cardTransactionsResource.resource.reload();
|
||||
if (!this.#cardTransactionsResource.resource.isLoading()) {
|
||||
this.#cardTransactionsResource.resource.reload();
|
||||
}
|
||||
|
||||
if (!this.#bonusCardsResource.resource.isLoading()) {
|
||||
this.#bonusCardsResource.resource.reload();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,159 +1,149 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
signal,
|
||||
computed,
|
||||
input,
|
||||
inject,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { PositiveIntegerInputDirective } from '@isa/utils/positive-integer-input';
|
||||
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';
|
||||
import { TooltipIconComponent } from '@isa/ui/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'crm-customer-booking',
|
||||
imports: [
|
||||
FormsModule,
|
||||
PositiveIntegerInputDirective,
|
||||
ButtonComponent,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
TooltipIconComponent,
|
||||
],
|
||||
templateUrl: './crm-feature-customer-booking.component.html',
|
||||
styleUrl: './crm-feature-customer-booking.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [CustomerBookingReasonsResource],
|
||||
})
|
||||
export class CrmFeatureCustomerBookingComponent implements OnDestroy {
|
||||
#reloadTimeoutId?: ReturnType<typeof setTimeout>;
|
||||
#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() {
|
||||
this.#reloadTimeoutId = setTimeout(() => {
|
||||
this.#transactionResource.resource.reload();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.#reloadTimeoutId) {
|
||||
clearTimeout(this.#reloadTimeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
signal,
|
||||
computed,
|
||||
input,
|
||||
inject,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { PositiveIntegerInputDirective } from '@isa/utils/positive-integer-input';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import {
|
||||
CustomerBookingReasonsResource,
|
||||
CustomerCardBookingFacade,
|
||||
} 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';
|
||||
import { TooltipIconComponent } from '@isa/ui/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'crm-customer-booking',
|
||||
imports: [
|
||||
FormsModule,
|
||||
PositiveIntegerInputDirective,
|
||||
ButtonComponent,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
TooltipIconComponent,
|
||||
],
|
||||
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);
|
||||
#errorFeedbackDialog = injectFeedbackErrorDialog();
|
||||
#feedbackDialog = injectFeedbackDialog();
|
||||
readonly cardCode = input<string | undefined>(undefined);
|
||||
|
||||
readonly booked = output<void>();
|
||||
|
||||
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.booked.emit();
|
||||
} catch (error: unknown) {
|
||||
this.#logger.error('Booking Failed', () => ({ error }));
|
||||
this.#errorFeedbackDialog({
|
||||
data: {
|
||||
errorMessage:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Buchen/Stornieren fehlgeschlagen',
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.isBooking.set(false);
|
||||
this.resetInputs();
|
||||
}
|
||||
}
|
||||
|
||||
resetInputs() {
|
||||
this.points.set(undefined);
|
||||
this.selectedReasonKey.set(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
input,
|
||||
effect,
|
||||
computed,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
||||
import { CdkTableModule } from '@angular/cdk/table';
|
||||
@@ -53,6 +54,8 @@ export class CrmFeatureCustomerCardTransactionsComponent {
|
||||
*/
|
||||
readonly cardCode = input<string | undefined>(undefined);
|
||||
|
||||
readonly reload = output<void>();
|
||||
|
||||
/**
|
||||
* Exposed resource signals for template
|
||||
*/
|
||||
@@ -90,8 +93,11 @@ export class CrmFeatureCustomerCardTransactionsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits reload event to notify parent component.
|
||||
* Parent is responsible for reloading resources to coordinate multi-resource updates.
|
||||
*/
|
||||
refresh() {
|
||||
this.#transactionsResource.params({ cardCode: this.cardCode() });
|
||||
this.#transactionsResource.resource.reload();
|
||||
this.reload.emit();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user