chore: add husky pre-commit hook and lint-staged configuration

- Created a pre-commit hook in .husky to run lint-staged.
- Added .lintstagedrc.mjs to format frontend files with Prettier and apply Spotless to backend Java files.
- Introduced .prettierignore to exclude specific directories from formatting.
This commit is contained in:
nessar committed 2026-08-17 14:18:16 +02:00
1 parent 5d2d85aa8c
commit 878da155bd
564 files changed
+61467 -47179

No files matched your search

@@ -1,29 +1,29 @@
:host {
display: block;
display: block;
}
.form-actions {
margin-top: 28px;
margin-top: 28px;
}
.form-section + .form-section {
margin-top: 30px;
padding-top: 28px;
border-top: 1px solid var(--border);
margin-top: 30px;
padding-top: 28px;
border-top: 1px solid var(--border);
}
.section-heading {
margin-bottom: 20px;
margin-bottom: 20px;
}
.section-heading h2 {
margin: 0 0 4px;
font-size: 1rem;
margin: 0 0 4px;
font-size: 1rem;
}
.section-heading p {
margin: 0;
color: var(--text-muted);
font-size: 0.8rem;
line-height: 1.6;
margin: 0;
color: var(--text-muted);
font-size: 0.8rem;
line-height: 1.6;
}
@media (min-width: 1180px) {
.form-page {
max-width: 980px;
}
.form-page {
max-width: 980px;
}
}
@@ -1,100 +1,100 @@
<div class="form-page">
<header class="page-heading form-heading">
<div>
<button
mat-button
type="button"
class="back-action"
[disabled]="saving()"
(click)="closed.emit()"
>
← Retour
</button>
<p class="eyebrow">Portefeuille</p>
<h1>{{ apartment() ? "Modifier l'appartement" : 'Ajouter un appartement' }}</h1>
<p>Renseignez les informations principales et juridiques du bien.</p>
</div>
</header>
<form
[formRoot]="apartmentForm"
(submit)="save(); $event.preventDefault()"
class="apartment-form"
[attr.aria-busy]="saving()"
>
@if (saveError()) {
<div class="alert-error" role="alert">{{ saveError() }}</div>
}
@if (error()) {
<div class="alert-error" role="alert">{{ error() }}</div>
}
<p class="visually-hidden" role="status" aria-live="polite">
{{ saving() ? 'Enregistrement en cours.' : '' }}
</p>
<fieldset class="busy-fieldset" [disabled]="saving()" [attr.inert]="saving() ? '' : null">
<section class="form-section" aria-labelledby="general-heading">
<div class="section-heading">
<h2 id="general-heading">Informations générales</h2>
<p>Les informations utilisées dans votre portefeuille.</p>
</div>
<div class="form-grid">
<mat-form-field appearance="outline"
><mat-label>Nom de l'appartement</mat-label
><input
matInput
type="text"
id="name"
[formField]="apartmentForm.name"
placeholder="T2 Paris 15"
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Prix d'acquisition</mat-label
><input
matInput
type="number"
id="purchasePrice"
[formField]="apartmentForm.purchasePrice"
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Adresse complète</mat-label
><input
matInput
type="text"
id="address"
[formField]="apartmentForm.address"
placeholder="45 rue des Entrepreneurs, 75015 Paris"
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Description ou notes</mat-label
><textarea
matInput
id="description"
[formField]="apartmentForm.description"
rows="4"
placeholder="Étage, agencement, équipements…"
></textarea>
</mat-form-field>
</div>
</section>
<header class="page-heading form-heading">
<div>
<button
mat-button
type="button"
class="back-action"
[disabled]="saving()"
(click)="closed.emit()"
>
← Retour
</button>
<p class="eyebrow">Portefeuille</p>
<h1>{{ apartment() ? "Modifier l'appartement" : 'Ajouter un appartement' }}</h1>
<p>Renseignez les informations principales et juridiques du bien.</p>
</div>
</header>
<form
[formRoot]="apartmentForm"
(submit)="save(); $event.preventDefault()"
class="apartment-form"
[attr.aria-busy]="saving()"
>
@if (saveError()) {
<div class="alert-error" role="alert">{{ saveError() }}</div>
}
@if (error()) {
<div class="alert-error" role="alert">{{ error() }}</div>
}
<p class="visually-hidden" role="status" aria-live="polite">
{{ saving() ? 'Enregistrement en cours.' : '' }}
</p>
<fieldset class="busy-fieldset" [disabled]="saving()" [attr.inert]="saving() ? '' : null">
<section class="form-section" aria-labelledby="general-heading">
<div class="section-heading">
<h2 id="general-heading">Informations générales</h2>
<p>Les informations utilisées dans votre portefeuille.</p>
</div>
<div class="form-grid">
<mat-form-field appearance="outline"
><mat-label>Nom de l'appartement</mat-label
><input
matInput
type="text"
id="name"
[formField]="apartmentForm.name"
placeholder="T2 Paris 15"
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Prix d'acquisition</mat-label
><input
matInput
type="number"
id="purchasePrice"
[formField]="apartmentForm.purchasePrice"
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Adresse complète</mat-label
><input
matInput
type="text"
id="address"
[formField]="apartmentForm.address"
placeholder="45 rue des Entrepreneurs, 75015 Paris"
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Description ou notes</mat-label
><textarea
matInput
id="description"
[formField]="apartmentForm.description"
rows="4"
placeholder="Étage, agencement, équipements…"
></textarea>
</mat-form-field>
</div>
</section>
<app-apartment-legal-profile-form [legalProfileForm]="apartmentForm.legalProfile" />
<app-private-area-editor
[areas]="apartmentModel().privateAreas"
(areasChanged)="setPrivateAreas($event)"
/>
<app-apartment-legal-profile-form [legalProfileForm]="apartmentForm.legalProfile" />
<app-private-area-editor
[areas]="apartmentModel().privateAreas"
(areasChanged)="setPrivateAreas($event)"
/>
<div class="form-actions">
<button mat-button type="button" (click)="closed.emit()">Annuler</button
><button mat-flat-button type="submit" class="primary-action">
<span>
@if (saving()) {
<mat-spinner diameter="18" aria-hidden="true" /> Enregistrement…
} @else {
Enregistrer
}
</span>
</button>
</div>
</fieldset>
</form>
<div class="form-actions">
<button mat-button type="button" (click)="closed.emit()">Annuler</button
><button mat-flat-button type="submit" class="primary-action">
<span>
@if (saving()) {
<mat-spinner diameter="18" aria-hidden="true" /> Enregistrement…
} @else {
Enregistrer
}
</span>
</button>
</div>
</fieldset>
</form>
</div>
@@ -4,244 +4,244 @@ import { ApartmentForm } from './apartment-form';
import { emptyApartmentLegalProfile } from '../../services/apartment.service';
describe('ApartmentForm', () => {
const completeLegalProfile = {
taxIdentifier: '751150001234',
buildingType: 'APARTMENT_BUILDING' as const,
ownershipRegime: 'Copropriété',
constructionPeriod: '1990_2005' as const,
habitableSurface: 42,
equipment: 'Cuisine équipée',
heating: 'Individuel électrique',
hotWater: 'Ballon électrique',
energyRating: 'D' as const,
estimatedAnnualEnergyCost: 850,
estimatedAnnualEnergyCostReferenceYear: 2021,
destination: 'Habitation',
privateAccessories: 'Cave',
commonAreas: 'Local vélos',
internetAccess: 'Fibre',
departmentCode: '75',
};
const completeLegalProfile = {
taxIdentifier: '751150001234',
buildingType: 'APARTMENT_BUILDING' as const,
ownershipRegime: 'Copropriété',
constructionPeriod: '1990_2005' as const,
habitableSurface: 42,
equipment: 'Cuisine équipée',
heating: 'Individuel électrique',
hotWater: 'Ballon électrique',
energyRating: 'D' as const,
estimatedAnnualEnergyCost: 850,
estimatedAnnualEnergyCostReferenceYear: 2021,
destination: 'Habitation',
privateAccessories: 'Cave',
commonAreas: 'Local vélos',
internetAccess: 'Fibre',
departmentCode: '75',
};
it('initializes its fields from the apartment being edited', () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apt-1',
name: 'T2 Paris 15',
address: '45 rue des Entrepreneurs, 75015 Paris',
purchasePrice: 250000,
description: 'Cinquième étage',
privateAreas: [],
legalProfile: completeLegalProfile,
});
fixture.detectChanges();
it('initializes its fields from the apartment being edited', () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apt-1',
name: 'T2 Paris 15',
address: '45 rue des Entrepreneurs, 75015 Paris',
purchasePrice: 250000,
description: 'Cinquième étage',
privateAreas: [],
legalProfile: completeLegalProfile,
});
fixture.detectChanges();
const form = fixture.componentInstance;
expect(form.apartmentModel().name).toBe('T2 Paris 15');
expect(form.apartmentModel().purchasePrice).toBe(250000);
expect(form.apartmentModel().legalProfile).toEqual(completeLegalProfile);
});
const form = fixture.componentInstance;
expect(form.apartmentModel().name).toBe('T2 Paris 15');
expect(form.apartmentModel().purchasePrice).toBe(250000);
expect(form.apartmentModel().legalProfile).toEqual(completeLegalProfile);
});
it('shows a validation error instead of emitting an incomplete apartment', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
it('shows a validation error instead of emitting an incomplete apartment', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.save();
form.save();
expect(form.error()).toContain('Veuillez remplir tous les champs obligatoires');
expect(saved).not.toHaveBeenCalled();
});
expect(form.error()).toContain('Veuillez remplir tous les champs obligatoires');
expect(saved).not.toHaveBeenCalled();
});
it('rejects whitespace-only fields before the backend does', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' ',
address: ' ',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
it('rejects whitespace-only fields before the backend does', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' ',
address: ' ',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
form.save();
form.save();
expect(form.error()).toContain('Veuillez remplir tous les champs obligatoires');
expect(saved).not.toHaveBeenCalled();
});
expect(form.error()).toContain('Veuillez remplir tous les champs obligatoires');
expect(saved).not.toHaveBeenCalled();
});
it('emits a trimmed apartment draft when valid', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' T2 Paris 15 ',
address: ' 45 rue des Entrepreneurs ',
purchasePrice: 250000,
description: ' Cinquième étage ',
privateAreas: [],
legalProfile: {
...completeLegalProfile,
taxIdentifier: ' 751150001234 ',
equipment: ' Cuisine équipée ',
departmentCode: ' 2a ',
},
});
it('emits a trimmed apartment draft when valid', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' T2 Paris 15 ',
address: ' 45 rue des Entrepreneurs ',
purchasePrice: 250000,
description: ' Cinquième étage ',
privateAreas: [],
legalProfile: {
...completeLegalProfile,
taxIdentifier: ' 751150001234 ',
equipment: ' Cuisine équipée ',
departmentCode: ' 2a ',
},
});
form.save();
form.save();
expect(saved).toHaveBeenCalledWith({
id: undefined,
name: 'T2 Paris 15',
address: '45 rue des Entrepreneurs',
purchasePrice: 250000,
description: 'Cinquième étage',
privateAreas: [],
legalProfile: {
...completeLegalProfile,
taxIdentifier: '751150001234',
equipment: 'Cuisine équipée',
departmentCode: '2A',
},
});
});
expect(saved).toHaveBeenCalledWith({
id: undefined,
name: 'T2 Paris 15',
address: '45 rue des Entrepreneurs',
purchasePrice: 250000,
description: 'Cinquième étage',
privateAreas: [],
legalProfile: {
...completeLegalProfile,
taxIdentifier: '751150001234',
equipment: 'Cuisine équipée',
departmentCode: '2A',
},
});
});
it('disables the whole form and preserves a save error while pending', () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saving', true);
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
it('disables the whole form and preserves a save error while pending', () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saving', true);
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('fieldset').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('[role=status]').closest('fieldset')).toBeNull();
expect(fixture.nativeElement.textContent).toContain('Enregistrement…');
expect(fixture.nativeElement.textContent).toContain(
'Impossible d’enregistrer cet appartement.',
);
});
expect(fixture.nativeElement.querySelector('fieldset').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('[role=status]').closest('fieldset')).toBeNull();
expect(fixture.nativeElement.textContent).toContain('Enregistrement…');
expect(fixture.nativeElement.textContent).toContain(
'Impossible d’enregistrer cet appartement.',
);
});
it('preserves entered values after a save fails', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
form.apartmentModel.set({
name: 'T2 rénové',
address: '45 rue des Entrepreneurs',
purchasePrice: 265000,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
await fixture.whenStable();
it('preserves entered values after a save fails', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
form.apartmentModel.set({
name: 'T2 rénové',
address: '45 rue des Entrepreneurs',
purchasePrice: 265000,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('input#name').value).toBe('T2 rénové');
expect(fixture.nativeElement.querySelector('input#address').value).toBe(
'45 rue des Entrepreneurs',
);
expect(fixture.nativeElement.textContent).toContain(
'Impossible d’enregistrer cet appartement.',
);
});
expect(fixture.nativeElement.querySelector('input#name').value).toBe('T2 rénové');
expect(fixture.nativeElement.querySelector('input#address').value).toBe(
'45 rue des Entrepreneurs',
);
expect(fixture.nativeElement.textContent).toContain(
'Impossible d’enregistrer cet appartement.',
);
});
it('rejects negative legal-profile numbers', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: { ...emptyApartmentLegalProfile(), habitableSurface: -1 },
});
it('rejects negative legal-profile numbers', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: { ...emptyApartmentLegalProfile(), habitableSurface: -1 },
});
form.save();
form.save();
expect(form.error()).toContain('surface habitable');
expect(saved).not.toHaveBeenCalled();
});
expect(form.error()).toContain('surface habitable');
expect(saved).not.toHaveBeenCalled();
});
it('turns an emptied nullable number field back into null', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const surfaceInput: HTMLInputElement =
fixture.nativeElement.querySelector('input#habitableSurface');
it('turns an emptied nullable number field back into null', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const surfaceInput: HTMLInputElement =
fixture.nativeElement.querySelector('input#habitableSurface');
surfaceInput.value = '42';
surfaceInput.dispatchEvent(new Event('input'));
surfaceInput.value = '';
surfaceInput.dispatchEvent(new Event('input'));
await fixture.whenStable();
surfaceInput.value = '42';
surfaceInput.dispatchEvent(new Event('input'));
surfaceInput.value = '';
surfaceInput.dispatchEvent(new Event('input'));
await fixture.whenStable();
expect(fixture.componentInstance.apartmentModel().legalProfile.habitableSurface).toBeNull();
});
expect(fixture.componentInstance.apartmentModel().legalProfile.habitableSurface).toBeNull();
});
it('asks for the energy reference year when an estimated cost is provided', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apartment-1',
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCost: 850,
},
});
fixture.detectChanges();
await fixture.whenStable();
const saved = vi.fn();
fixture.componentInstance.saved.subscribe(saved);
it('asks for the energy reference year when an estimated cost is provided', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apartment-1',
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCost: 850,
},
});
fixture.detectChanges();
await fixture.whenStable();
const saved = vi.fn();
fixture.componentInstance.saved.subscribe(saved);
fixture.nativeElement.querySelector('button[type=submit]').click();
fixture.detectChanges();
fixture.nativeElement.querySelector('button[type=submit]').click();
fixture.detectChanges();
expect(saved).not.toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain(
"Renseignez l'année de référence du coût énergétique.",
);
});
expect(saved).not.toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain(
"Renseignez l'année de référence du coût énergétique.",
);
});
it('clears an energy reference year that has no estimated cost', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apartment-1',
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCostReferenceYear: 2021,
},
});
fixture.detectChanges();
await fixture.whenStable();
const saved = vi.fn();
fixture.componentInstance.saved.subscribe(saved);
it('clears an energy reference year that has no estimated cost', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apartment-1',
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCostReferenceYear: 2021,
},
});
fixture.detectChanges();
await fixture.whenStable();
const saved = vi.fn();
fixture.componentInstance.saved.subscribe(saved);
fixture.nativeElement.querySelector('button[type=submit]').click();
fixture.nativeElement.querySelector('button[type=submit]').click();
expect(saved).toHaveBeenCalledWith(
expect.objectContaining({
legalProfile: expect.objectContaining({
estimatedAnnualEnergyCostReferenceYear: null,
}),
}),
);
});
expect(saved).toHaveBeenCalledWith(
expect.objectContaining({
legalProfile: expect.objectContaining({
estimatedAnnualEnergyCostReferenceYear: null,
}),
}),
);
});
});
@@ -6,137 +6,137 @@ 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,
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';
const requiredFieldsError =
'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).';
'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).';
@Component({
selector: 'app-apartment-form',
imports: [
FormsModule,
FormRoot,
FormField,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
ApartmentLegalProfileForm,
PrivateAreaEditor,
],
templateUrl: './apartment-form.html',
styleUrl: './apartment-form.css',
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 closed = output<void>();
readonly saved = output<Apartment>();
readonly apartment = input<Apartment | null>(null);
readonly saving = input(false);
readonly saveError = input('');
readonly closed = output<void>();
readonly saved = output<Apartment>();
readonly apartmentModel = signal<Apartment>({
name: '',
address: '',
purchasePrice: 0,
description: '',
legalProfile: emptyApartmentLegalProfile(),
privateAreas: [],
});
readonly apartmentModel = signal<Apartment>({
name: '',
address: '',
purchasePrice: 0,
description: '',
legalProfile: emptyApartmentLegalProfile(),
privateAreas: [],
});
readonly apartmentForm = form(this.apartmentModel, (f) => {
required(f.name, { message: requiredFieldsError });
required(f.address, { message: requiredFieldsError });
required(f.purchasePrice, { message: requiredFieldsError });
min(f.purchasePrice, 1, { message: requiredFieldsError });
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 apartmentForm = form(this.apartmentModel, (f) => {
required(f.name, { message: requiredFieldsError });
required(f.address, { message: requiredFieldsError });
required(f.purchasePrice, { message: requiredFieldsError });
min(f.purchasePrice, 1, { message: requiredFieldsError });
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('');
readonly error = signal('');
constructor() {
effect(() => this.resetForm(this.apartment()));
}
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 ?? requiredFieldsError);
return;
}
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 ?? requiredFieldsError);
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,
});
}
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 }));
}
setPrivateAreas(privateAreas: PrivateArea[]): void {
this.apartmentModel.update((apartment) => ({ ...apartment, privateAreas }));
}
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 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(),
};
}
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(),
};
}
}