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
@@ -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