195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
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<CoreField, ReadonlySet<string>> = {
|
||
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<Apartment | null>(null);
|
||
readonly saving = input(false);
|
||
readonly saveError = input('');
|
||
readonly saveFieldErrors = input<FieldValidationError[]>([]);
|
||
readonly closed = output<void>();
|
||
readonly saved = output<Apartment>();
|
||
readonly fieldChanged = output<string>();
|
||
|
||
readonly apartmentModel = signal<Apartment>({
|
||
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(),
|
||
};
|
||
}
|
||
}
|