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,8 +1,8 @@
:host {
display: block;
display: block;
}
@media (min-width: 1180px) {
.form-page {
max-width: 900px;
}
.form-page {
max-width: 900px;
}
}
@@ -1,104 +1,116 @@
<section class="form-page">
<header class="page-heading">
<div>
<button
mat-button
type="button"
class="back-action"
[disabled]="saving()"
(click)="cancelled.emit()"
>
← Retour
</button>
<p class="eyebrow">Dossier locataire</p>
<h1>{{ tenant() ? 'Modifier le locataire' : 'Ajouter un locataire' }}</h1>
<p>Renseignez les coordonnées nécessaires à la gestion du bail.</p>
</div>
</header>
<form class="entity-form" [formRoot]="tenantForm" (ngSubmit)="save()" [attr.aria-busy]="saving()">
@if (saveError()) {
<div class="alert-error" role="alert">{{ saveError() }}</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">
<div class="form-grid">
<mat-form-field appearance="outline"
><mat-label>Prénom</mat-label
><input
matInput
id="firstName"
[formField]="tenantForm.firstName"
[attr.aria-describedby]="
tenantForm.firstName().touched() && tenantForm.firstName().invalid()
? 'firstName-error'
: null
"
/>
@if (tenantForm.firstName().touched() && tenantForm.firstName().invalid()) {
<mat-error id="firstName-error">Le prénom est obligatoire.</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Nom</mat-label
><input
matInput
id="lastName"
[formField]="tenantForm.lastName"
[attr.aria-describedby]="
tenantForm.lastName().touched() && tenantForm.lastName().invalid()
? 'lastName-error'
: null
"
/>
@if (tenantForm.lastName().touched() && tenantForm.lastName().invalid()) {
<mat-error id="lastName-error">Le nom est obligatoire.</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline"
><mat-label>E-mail</mat-label
><input
matInput
type="email"
id="email"
[formField]="tenantForm.email"
autocomplete="email"
[attr.aria-describedby]="
tenantForm.email().touched() && tenantForm.email().invalid() ? 'email-error' : null
"
/>
@if (tenantForm.email().touched() && tenantForm.email().invalid()) {
<mat-error id="email-error">
@if (tenantForm.email().getError('required')) {
L’e-mail est obligatoire.
} @else {
Saisissez une adresse e-mail valide.
}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Téléphone</mat-label
><input matInput type="tel" id="phone" [formField]="tenantForm.phone" autocomplete="tel"
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Adresse complète</mat-label
><input matInput id="address" [formField]="tenantForm.address"
/></mat-form-field>
</div>
<div class="form-actions">
<button mat-button type="button" (click)="cancelled.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>
<header class="page-heading">
<div>
<button
mat-button
type="button"
class="back-action"
[disabled]="saving()"
(click)="cancelled.emit()"
>
← Retour
</button>
<p class="eyebrow">Dossier locataire</p>
<h1>{{ tenant() ? 'Modifier le locataire' : 'Ajouter un locataire' }}</h1>
<p>Renseignez les coordonnées nécessaires à la gestion du bail.</p>
</div>
</header>
<form
class="entity-form"
[formRoot]="tenantForm"
(ngSubmit)="save()"
[attr.aria-busy]="saving()"
>
@if (saveError()) {
<div class="alert-error" role="alert">{{ saveError() }}</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">
<div class="form-grid">
<mat-form-field appearance="outline"
><mat-label>Prénom</mat-label
><input
matInput
id="firstName"
[formField]="tenantForm.firstName"
[attr.aria-describedby]="
tenantForm.firstName().touched() && tenantForm.firstName().invalid()
? 'firstName-error'
: null
"
/>
@if (tenantForm.firstName().touched() && tenantForm.firstName().invalid()) {
<mat-error id="firstName-error">Le prénom est obligatoire.</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Nom</mat-label
><input
matInput
id="lastName"
[formField]="tenantForm.lastName"
[attr.aria-describedby]="
tenantForm.lastName().touched() && tenantForm.lastName().invalid()
? 'lastName-error'
: null
"
/>
@if (tenantForm.lastName().touched() && tenantForm.lastName().invalid()) {
<mat-error id="lastName-error">Le nom est obligatoire.</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline"
><mat-label>E-mail</mat-label
><input
matInput
type="email"
id="email"
[formField]="tenantForm.email"
autocomplete="email"
[attr.aria-describedby]="
tenantForm.email().touched() && tenantForm.email().invalid()
? 'email-error'
: null
"
/>
@if (tenantForm.email().touched() && tenantForm.email().invalid()) {
<mat-error id="email-error">
@if (tenantForm.email().getError('required')) {
L’e-mail est obligatoire.
} @else {
Saisissez une adresse e-mail valide.
}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Téléphone</mat-label
><input
matInput
type="tel"
id="phone"
[formField]="tenantForm.phone"
autocomplete="tel"
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Adresse complète</mat-label
><input matInput id="address" [formField]="tenantForm.address"
/></mat-form-field>
</div>
<div class="form-actions">
<button mat-button type="button" (click)="cancelled.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>
</section>
@@ -3,68 +3,68 @@ import { vi } from 'vitest';
import { TenantForm } from './tenant-form';
describe('TenantForm', () => {
it('emits a trimmed tenant through its small interface', () => {
const fixture = TestBed.createComponent(TenantForm);
const component = fixture.componentInstance;
component.tenantModel.set({
firstName: ' Camille ',
lastName: ' Martin ',
email: ' camille@example.com ',
phone: '0600000000',
address: '',
});
let firstName = '';
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
it('emits a trimmed tenant through its small interface', () => {
const fixture = TestBed.createComponent(TenantForm);
const component = fixture.componentInstance;
component.tenantModel.set({
firstName: ' Camille ',
lastName: ' Martin ',
email: ' camille@example.com ',
phone: '0600000000',
address: '',
});
let firstName = '';
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
component.save();
component.save();
expect(firstName).toBe('Camille');
});
expect(firstName).toBe('Camille');
});
it('loads an existing tenant and leaves invalid data untouched', () => {
const fixture = TestBed.createComponent(TenantForm);
fixture.componentRef.setInput('tenant', {
id: 'tenant-1',
firstName: 'Camille',
lastName: 'Martin',
email: 'camille@example.com',
phone: '0600000000',
address: '1 rue de Paris',
});
fixture.detectChanges();
const submitted = vi.fn();
fixture.componentInstance.submitted.subscribe(submitted);
it('loads an existing tenant and leaves invalid data untouched', () => {
const fixture = TestBed.createComponent(TenantForm);
fixture.componentRef.setInput('tenant', {
id: 'tenant-1',
firstName: 'Camille',
lastName: 'Martin',
email: 'camille@example.com',
phone: '0600000000',
address: '1 rue de Paris',
});
fixture.detectChanges();
const submitted = vi.fn();
fixture.componentInstance.submitted.subscribe(submitted);
// Make the form invalid (invalid email)
fixture.componentInstance.tenantModel.update((t) => ({ ...t, email: 'invalid-email' }));
fixture.componentInstance.save();
// Make the form invalid (invalid email)
fixture.componentInstance.tenantModel.update((t) => ({ ...t, email: 'invalid-email' }));
fixture.componentInstance.save();
expect(fixture.componentInstance.tenantModel().firstName).toBe('Camille');
expect(fixture.componentInstance.tenantForm().touched()).toBe(true);
expect(submitted).not.toHaveBeenCalled();
});
expect(fixture.componentInstance.tenantModel().firstName).toBe('Camille');
expect(fixture.componentInstance.tenantForm().touched()).toBe(true);
expect(submitted).not.toHaveBeenCalled();
});
it('shows actionable validation guidance linked to invalid fields', () => {
const fixture = TestBed.createComponent(TenantForm);
fixture.detectChanges();
fixture.componentInstance.tenantModel.set({
firstName: '',
lastName: 'Martin',
email: 'invalid-email',
phone: '',
address: '',
});
fixture.componentInstance.save();
fixture.detectChanges();
it('shows actionable validation guidance linked to invalid fields', () => {
const fixture = TestBed.createComponent(TenantForm);
fixture.detectChanges();
fixture.componentInstance.tenantModel.set({
firstName: '',
lastName: 'Martin',
email: 'invalid-email',
phone: '',
address: '',
});
fixture.componentInstance.save();
fixture.detectChanges();
const firstNameInput = fixture.nativeElement.querySelector('#firstName');
const emailInput = fixture.nativeElement.querySelector('#email');
const firstNameError = fixture.nativeElement.querySelector('#firstName-error');
const emailError = fixture.nativeElement.querySelector('#email-error');
const firstNameInput = fixture.nativeElement.querySelector('#firstName');
const emailInput = fixture.nativeElement.querySelector('#email');
const firstNameError = fixture.nativeElement.querySelector('#firstName-error');
const emailError = fixture.nativeElement.querySelector('#email-error');
expect(firstNameError?.textContent ?? '').toContain('Le prénom est obligatoire');
expect(emailError?.textContent ?? '').toContain('Saisissez une adresse e-mail valide');
expect(firstNameInput.getAttribute('aria-describedby')).toContain('firstName-error');
expect(emailInput.getAttribute('aria-describedby')).toContain('email-error');
});
expect(firstNameError?.textContent ?? '').toContain('Le prénom est obligatoire');
expect(emailError?.textContent ?? '').toContain('Saisissez une adresse e-mail valide');
expect(firstNameInput.getAttribute('aria-describedby')).toContain('firstName-error');
expect(emailInput.getAttribute('aria-describedby')).toContain('email-error');
});
});
@@ -8,86 +8,86 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Tenant } from '../../services/tenant.service';
export interface TenantSaveRequest {
tenant: Tenant;
tenantId?: string;
tenant: Tenant;
tenantId?: string;
}
@Component({
selector: 'app-tenant-form',
imports: [
FormsModule,
FormRoot,
FormField,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
],
templateUrl: './tenant-form.html',
styleUrl: './tenant-form.css',
selector: 'app-tenant-form',
imports: [
FormsModule,
FormRoot,
FormField,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
],
templateUrl: './tenant-form.html',
styleUrl: './tenant-form.css',
})
export class TenantForm {
readonly tenant = input<Tenant | null>(null);
readonly saving = input(false);
readonly saveError = input('');
readonly cancelled = output<void>();
readonly submitted = output<TenantSaveRequest>();
readonly tenant = input<Tenant | null>(null);
readonly saving = input(false);
readonly saveError = input('');
readonly cancelled = output<void>();
readonly submitted = output<TenantSaveRequest>();
readonly tenantModel = signal<Tenant>({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
});
readonly tenantModel = signal<Tenant>({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
});
readonly tenantForm = form(this.tenantModel, (f) => {
required(f.firstName);
required(f.lastName);
required(f.email);
email(f.email);
});
readonly tenantForm = form(this.tenantModel, (f) => {
required(f.firstName);
required(f.lastName);
required(f.email);
email(f.email);
});
constructor() {
effect(() => this.fillForm(this.tenant()));
}
constructor() {
effect(() => this.fillForm(this.tenant()));
}
save(): void {
if (this.saving()) return;
this.tenantModel.update((m) => ({
firstName: m.firstName.trim(),
lastName: m.lastName.trim(),
email: m.email.trim(),
phone: m.phone.trim(),
address: m.address.trim(),
}));
save(): void {
if (this.saving()) return;
this.tenantModel.update((m) => ({
firstName: m.firstName.trim(),
lastName: m.lastName.trim(),
email: m.email.trim(),
phone: m.phone.trim(),
address: m.address.trim(),
}));
this.tenantForm().markAsTouched();
if (this.tenantForm().invalid()) {
return;
}
this.tenantForm().markAsTouched();
if (this.tenantForm().invalid()) {
return;
}
const modelVal = this.tenantModel();
this.submitted.emit({
tenant: {
firstName: modelVal.firstName,
lastName: modelVal.lastName,
email: modelVal.email,
phone: modelVal.phone,
address: modelVal.address,
},
tenantId: this.tenant()?.id,
});
}
const modelVal = this.tenantModel();
this.submitted.emit({
tenant: {
firstName: modelVal.firstName,
lastName: modelVal.lastName,
email: modelVal.email,
phone: modelVal.phone,
address: modelVal.address,
},
tenantId: this.tenant()?.id,
});
}
private fillForm(tenant: Tenant | null): void {
this.tenantModel.set({
id: tenant?.id,
firstName: tenant?.firstName ?? '',
lastName: tenant?.lastName ?? '',
email: tenant?.email ?? '',
phone: tenant?.phone ?? '',
address: tenant?.address ?? '',
});
}
private fillForm(tenant: Tenant | null): void {
this.tenantModel.set({
id: tenant?.id,
firstName: tenant?.firstName ?? '',
lastName: tenant?.lastName ?? '',
email: tenant?.email ?? '',
phone: tenant?.phone ?? '',
address: tenant?.address ?? '',
});
}
}