Align rent payment dialog validation (#55)
This commit is contained in:
1 parent
cd32d05466
commit
96bd02e3a6
23 files changed
+723
-87
No files matched your search
+22
-6
@@ -8,9 +8,21 @@
|
||||
<fieldset class="dialog-fieldset" [disabled]="saving()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Date du remboursement</mat-label>
|
||||
<input matInput type="date" formControlName="refundedAt" [max]="today" required />
|
||||
@if (form.controls.refundedAt.hasError('futureDate')) {
|
||||
<input
|
||||
matInput
|
||||
type="date"
|
||||
formControlName="refundedAt"
|
||||
[max]="today"
|
||||
required
|
||||
(input)="clearFieldError('refundedAt')"
|
||||
/>
|
||||
@if (
|
||||
form.controls.refundedAt.hasError('futureDate') ||
|
||||
form.controls.refundedAt.hasError('future_date')
|
||||
) {
|
||||
<mat-error>La date du remboursement ne peut pas être dans le futur.</mat-error>
|
||||
} @else if (form.controls.refundedAt.invalid) {
|
||||
<mat-error>La date du remboursement est obligatoire.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
@@ -18,13 +30,17 @@
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
step="any"
|
||||
min="0"
|
||||
formControlName="amount"
|
||||
required
|
||||
(input)="clearFieldError('amount')"
|
||||
/>
|
||||
<span matTextSuffix>€</span>
|
||||
@if (form.controls.amount.hasError('max')) {
|
||||
@if (
|
||||
form.controls.amount.hasError('max') ||
|
||||
form.controls.amount.hasError('exceeds_unallocated_balance')
|
||||
) {
|
||||
<mat-error>Le remboursement dépasse le solde non affecté.</mat-error>
|
||||
} @else if (form.controls.amount.invalid) {
|
||||
<mat-error>Le montant doit être strictement positif.</mat-error>
|
||||
@@ -37,7 +53,7 @@
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close type="button" [disabled]="saving()">Annuler</button>
|
||||
<button mat-flat-button type="submit" [disabled]="form.invalid || saving()">
|
||||
<button mat-flat-button type="submit" [disabled]="saving()">
|
||||
@if (saving()) {
|
||||
<mat-spinner diameter="18" aria-label="Remboursement en cours" />
|
||||
}
|
||||
|
||||
+46
-7
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user