Files
lmnp-organizer/frontend/src/app/components/rent-payment-refund-dialog/rent-payment-refund-dialog.ts
T

140 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, ElementRef, inject, signal } from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { finalize } from 'rxjs';
import { saveFailure } from '../../services/http-error-message';
import { browserTimeZone, formatEuro, localIsoDate } from '../../services/rent-payment-format';
import { RentPayment, RentPaymentService } from '../../services/rent-payment.service';
import {
isRentPaymentConflict,
isRentPaymentDataStale,
RENT_PAYMENT_CONFLICT_MESSAGE,
RENT_PAYMENT_DATA_STALE,
} from '../rent-payment-dialog-result';
@Component({
selector: 'app-rent-payment-refund-dialog',
imports: [
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
],
templateUrl: './rent-payment-refund-dialog.html',
styleUrl: './rent-payment-refund-dialog.css',
})
export class RentPaymentRefundDialog {
readonly payment = inject<RentPayment>(MAT_DIALOG_DATA);
private readonly dialogRef = inject(MatDialogRef<RentPaymentRefundDialog>);
private readonly payments = inject(RentPaymentService);
private readonly host = inject(ElementRef<HTMLElement>);
private readonly operationId = crypto.randomUUID();
readonly today = localIsoDate();
readonly saving = signal(false);
readonly error = signal('');
readonly fieldErrorFields = signal<ReadonlySet<string>>(new Set());
readonly formatEuro = formatEuro;
readonly form = new FormGroup({
refundedAt: new FormControl(this.today, {
nonNullable: true,
validators: [Validators.required, this.notFuture.bind(this)],
}),
amount: new FormControl(this.payment.unallocatedBalance, {
nonNullable: true,
validators: [
Validators.required,
Validators.min(Number.MIN_VALUE),
Validators.max(this.payment.unallocatedBalance),
],
}),
});
submit(): void {
this.form.markAllAsTouched();
if (this.form.invalid) {
this.focusFirstInvalid();
return;
}
if (this.saving()) return;
this.saving.set(true);
this.error.set('');
this.payments
.refund(this.payment.id, {
operationId: this.operationId,
paymentVersion: this.payment.version,
timeZone: browserTimeZone,
...this.form.getRawValue(),
})
.pipe(finalize(() => this.saving.set(false)))
.subscribe({
next: (payment) => this.dialogRef.close(payment),
error: (error) => {
if (isRentPaymentDataStale(error)) {
this.dialogRef.close(RENT_PAYMENT_DATA_STALE);
return;
}
this.showFailure(error, 'Impossible d’enregistrer ce remboursement.');
},
});
}
private showFailure(error: unknown, fallback: string): void {
const failure = saveFailure(error, fallback);
const knownErrors = failure.fieldErrors.filter((fieldError) =>
this.isKnownFieldError(fieldError.field, fieldError.code),
);
this.error.set(
knownErrors.length > 0 && knownErrors.length === failure.fieldErrors.length
? ''
: isRentPaymentConflict(error)
? RENT_PAYMENT_CONFLICT_MESSAGE
: failure.message,
);
this.fieldErrorFields.set(new Set(knownErrors.map((fieldError) => fieldError.field)));
for (const fieldError of knownErrors) {
const control = this.form.get(fieldError.field);
if (control) control.setErrors({ ...control.errors, [fieldError.code]: true });
}
this.focusFirstInvalid();
}
clearFieldError(field: string): void {
if (!this.fieldErrorFields().has(field)) return;
const remaining = new Set(this.fieldErrorFields());
remaining.delete(field);
this.fieldErrorFields.set(remaining);
if (remaining.size === 0) this.error.set('');
}
private isKnownFieldError(field: string, code: string): boolean {
return (
(field === 'refundedAt' && ['not_null', 'future_date'].includes(code)) ||
(field === 'amount' &&
['not_null', 'positive', 'exceeds_unallocated_balance'].includes(code))
);
}
private focusFirstInvalid(): void {
const field = ['refundedAt', 'amount'].find((name) => this.form.get(name)?.invalid);
if (field) this.host.nativeElement.querySelector(`[formControlName="${field}"]`)?.focus();
}
private notFuture(control: AbstractControl<string>): ValidationErrors | null {
return control.value > this.today ? { futureDate: true } : null;
}
}