import { Component, effect, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FormRoot, FormField, form, max, min, required } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { Apartment, ApartmentLegalProfile, emptyApartmentLegalProfile, PrivateArea, } from '../../services/apartment.service'; import { ApartmentLegalProfileForm } from '../apartment-legal-profile-form/apartment-legal-profile-form'; import { PrivateAreaEditor } from '../private-area-editor/private-area-editor'; import { FieldValidationError } from '../../services/http-error-message'; const nameRequiredError = 'Le nom de l’appartement est obligatoire.'; const addressRequiredError = 'L’adresse complète est obligatoire.'; const purchasePriceError = 'Le prix d’acquisition doit être strictement positif.'; type CoreField = 'name' | 'address' | 'purchasePrice'; const serverErrorCodes: Record> = { name: new Set(['not_blank']), address: new Set(['not_blank']), purchasePrice: new Set(['positive']), }; @Component({ selector: 'app-apartment-form', imports: [ FormsModule, FormRoot, FormField, MatButtonModule, MatFormFieldModule, MatInputModule, MatProgressSpinnerModule, ApartmentLegalProfileForm, PrivateAreaEditor, ], templateUrl: './apartment-form.html', styleUrl: './apartment-form.css', }) export class ApartmentForm { readonly apartment = input(null); readonly saving = input(false); readonly saveError = input(''); readonly saveFieldErrors = input([]); readonly closed = output(); readonly saved = output(); readonly fieldChanged = output(); readonly apartmentModel = signal({ name: '', address: '', purchasePrice: 0, description: '', legalProfile: emptyApartmentLegalProfile(), privateAreas: [], }); readonly apartmentForm = form(this.apartmentModel, (f) => { required(f.name, { message: nameRequiredError }); required(f.address, { message: addressRequiredError }); required(f.purchasePrice, { message: purchasePriceError }); min(f.purchasePrice, Number.MIN_VALUE, { message: purchasePriceError }); min(f.legalProfile.habitableSurface, 0, { message: 'La surface habitable doit être positive ou nulle.', }); min(f.legalProfile.estimatedAnnualEnergyCost, 0, { message: 'Le coût annuel énergétique doit être positif ou nul.', }); required(f.legalProfile.estimatedAnnualEnergyCostReferenceYear, { when: ({ valueOf }) => valueOf(f.legalProfile.estimatedAnnualEnergyCost) !== null, message: "Renseignez l'année de référence du coût énergétique.", }); min(f.legalProfile.estimatedAnnualEnergyCostReferenceYear, 1900, { message: "L'année de référence énergétique doit être comprise entre 1900 et 2100.", }); max(f.legalProfile.estimatedAnnualEnergyCostReferenceYear, 2100, { message: "L'année de référence énergétique doit être comprise entre 1900 et 2100.", }); }); readonly error = signal(''); constructor() { effect(() => this.resetForm(this.apartment())); } save(): void { if (this.saving()) return; this.apartmentForm().markAsTouched(); if ( this.apartmentForm().invalid() || !this.apartmentModel().name.trim() || !this.apartmentModel().address.trim() ) { const firstError = this.apartmentForm().errorSummary()[0]; this.error.set(firstError?.message ?? this.coreFieldError() ?? nameRequiredError); return; } this.saved.emit({ id: this.apartment()?.id, name: this.apartmentModel().name.trim(), address: this.apartmentModel().address.trim(), purchasePrice: this.apartmentModel().purchasePrice, description: this.apartmentModel().description.trim(), legalProfile: this.normalizedLegalProfile(), privateAreas: this.apartmentModel().privateAreas, }); } setPrivateAreas(privateAreas: PrivateArea[]): void { this.apartmentModel.update((apartment) => ({ ...apartment, privateAreas })); } nameError(): string { return this.fieldError('name', nameRequiredError, this.apartmentForm.name().touched()); } addressError(): string { return this.fieldError( 'address', addressRequiredError, this.apartmentForm.address().touched(), ); } purchasePriceError(): string { return this.fieldError( 'purchasePrice', purchasePriceError, this.apartmentForm.purchasePrice().touched(), ); } private coreFieldError(): string | null { return this.nameError() || this.addressError() || this.purchasePriceError() || null; } private fieldError(path: CoreField, clientMessage: string, touched: boolean): string { if ( touched && ((path === 'name' && !this.apartmentModel().name.trim()) || (path === 'address' && !this.apartmentModel().address.trim()) || (path === 'purchasePrice' && this.apartmentModel().purchasePrice <= 0)) ) { return clientMessage; } return this.hasKnownServerError(path) ? clientMessage : ''; } private hasKnownServerError(field: CoreField): boolean { return this.saveFieldErrors().some( (error) => error.field === field && serverErrorCodes[field].has(error.code), ); } private resetForm(apartment: Apartment | null): void { this.apartmentModel.set({ id: apartment?.id, name: apartment?.name ?? '', address: apartment?.address ?? '', purchasePrice: apartment?.purchasePrice ?? 0, description: apartment?.description ?? '', legalProfile: apartment?.legalProfile ?? emptyApartmentLegalProfile(), privateAreas: apartment?.privateAreas ?? [], }); this.error.set(''); } private normalizedLegalProfile(): ApartmentLegalProfile { const profile = this.apartmentModel().legalProfile; return { ...profile, estimatedAnnualEnergyCostReferenceYear: profile.estimatedAnnualEnergyCost === null ? null : profile.estimatedAnnualEnergyCostReferenceYear, taxIdentifier: profile.taxIdentifier.trim(), ownershipRegime: profile.ownershipRegime.trim(), equipment: profile.equipment.trim(), heating: profile.heating.trim(), hotWater: profile.hotWater.trim(), destination: profile.destination.trim(), privateAccessories: profile.privateAccessories.trim(), commonAreas: profile.commonAreas.trim(), internetAccess: profile.internetAccess.trim(), departmentCode: profile.departmentCode.trim().toUpperCase(), }; } }