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 { isRentPaymentDataStale, 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(MAT_DIALOG_DATA); private readonly dialogRef = inject(MatDialogRef); private readonly payments = inject(RentPaymentService); private readonly host = inject(ElementRef); private readonly operationId = crypto.randomUUID(); readonly today = localIsoDate(); readonly saving = signal(false); readonly error = signal(''); readonly fieldErrorFields = signal>(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 === 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): ValidationErrors | null { return control.value > this.today ? { futureDate: true } : null; } }