116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
import { Component, ElementRef, inject, signal } from '@angular/core';
|
|
import {
|
|
AbstractControl,
|
|
FormControl,
|
|
FormGroup,
|
|
ReactiveFormsModule,
|
|
ValidationErrors,
|
|
} 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 } from '../../services/rent-payment-format';
|
|
import { RentPayment, RentPaymentService } from '../../services/rent-payment.service';
|
|
import { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
|
|
|
|
@Component({
|
|
selector: 'app-rent-payment-reversal-dialog',
|
|
imports: [
|
|
ReactiveFormsModule,
|
|
MatButtonModule,
|
|
MatDialogModule,
|
|
MatFormFieldModule,
|
|
MatInputModule,
|
|
MatProgressSpinnerModule,
|
|
],
|
|
templateUrl: './rent-payment-reversal-dialog.html',
|
|
styleUrl: './rent-payment-reversal-dialog.css',
|
|
})
|
|
export class RentPaymentReversalDialog {
|
|
readonly payment = inject<RentPayment>(MAT_DIALOG_DATA);
|
|
private readonly dialogRef = inject(MatDialogRef<RentPaymentReversalDialog>);
|
|
private readonly payments = inject(RentPaymentService);
|
|
private readonly host = inject(ElementRef<HTMLElement>);
|
|
readonly saving = signal(false);
|
|
readonly error = signal('');
|
|
readonly fieldErrorFields = signal<ReadonlySet<string>>(new Set());
|
|
readonly formatEuro = formatEuro;
|
|
|
|
readonly form = new FormGroup({
|
|
reason: new FormControl('', {
|
|
nonNullable: true,
|
|
validators: this.validReason.bind(this),
|
|
}),
|
|
});
|
|
|
|
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
|
|
.reverse(
|
|
this.payment.id,
|
|
this.payment.version,
|
|
this.form.controls.reason.value,
|
|
browserTimeZone,
|
|
)
|
|
.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 de contrepasser ce règlement.');
|
|
},
|
|
});
|
|
}
|
|
|
|
private validReason(control: AbstractControl<string>): ValidationErrors | null {
|
|
const reason = control.value.trim();
|
|
if (!reason) return { required: true };
|
|
return reason.length > 500 ? { maxlength: true } : null;
|
|
}
|
|
|
|
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 === 'reason' && ['not_blank', 'size'].includes(code);
|
|
}
|
|
|
|
private focusFirstInvalid(): void {
|
|
if (this.form.controls.reason.invalid)
|
|
this.host.nativeElement.querySelector('[formControlName="reason"]')?.focus();
|
|
}
|
|
}
|