Align rent payment dialog validation (#55)

This commit is contained in:
nessar committed 2026-08-17 14:18:16 +02:00
1 parent cd32d05466
commit 96bd02e3a6
23 files changed
+723 -87

No files matched your search

@@ -1,4 +1,4 @@
import { Component, inject, signal } from '@angular/core';
import { Component, ElementRef, inject, signal } from '@angular/core';
import {
AbstractControl,
FormControl,
@@ -13,7 +13,7 @@ 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 { httpErrorMessage } from '../../services/http-error-message';
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 { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
@@ -35,10 +35,12 @@ 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({
@@ -50,7 +52,7 @@ export class RentPaymentRefundDialog {
nonNullable: true,
validators: [
Validators.required,
Validators.min(0.01),
Validators.min(Number.MIN_VALUE),
Validators.max(this.payment.unallocatedBalance),
],
}),
@@ -58,7 +60,11 @@ export class RentPaymentRefundDialog {
submit(): void {
this.form.markAllAsTouched();
if (this.form.invalid || this.saving()) return;
if (this.form.invalid) {
this.focusFirstInvalid();
return;
}
if (this.saving()) return;
this.saving.set(true);
this.error.set('');
this.payments
@@ -76,13 +82,46 @@ export class RentPaymentRefundDialog {
this.dialogRef.close(RENT_PAYMENT_DATA_STALE);
return;
}
this.error.set(
httpErrorMessage(error, 'Impossible d’enregistrer ce remboursement.'),
);
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 === failure.fieldErrors.length ? '' : 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;
}