Files
lmnp-organizer/frontend/src/app/components/apartment-form/apartment-form.ts
T

195 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(),
};
}
}