feat: add comprehensive loading feedback (#18)

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent 45de0cebcc
commit ec3319494b
47 files changed
+1753 -406

No files matched your search

@@ -28,6 +28,17 @@
border-radius: 22px;
background: var(--surface);
}
fieldset {
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
.primary-action mat-spinner {
display: inline-block;
margin-right: 8px;
vertical-align: middle;
}
.guarantor-section {
margin-top: 18px;
}
@@ -1,7 +1,13 @@
<section class="form-page">
<header class="page-heading">
<div>
<button mat-button type="button" class="back-action" (click)="cancelled.emit()">
<button
mat-button
type="button"
class="back-action"
[disabled]="saving()"
(click)="cancelled.emit()"
>
← Retour
</button>
<p class="eyebrow">Nouveau bail</p>
@@ -15,141 +21,163 @@
#leaseForm="ngForm"
ngNativeValidate
(ngSubmit)="save(leaseForm)"
[attr.aria-busy]="saving()"
>
@if (saveError()) {
<div class="alert-error" role="alert">{{ saveError() }}</div>
}
@if (error()) {
<div class="alert-error" role="alert">{{ error() }}</div>
}
<section class="form-section">
<div class="section-heading">
<span>01</span>
<div>
<h2>Parties du bail</h2>
<p>Choisissez le logement et les locataires.</p>
<fieldset [disabled]="saving()" [attr.inert]="saving() ? '' : null">
<section class="form-section">
<div class="section-heading">
<span>01</span>
<div>
<h2>Parties du bail</h2>
<p>Choisissez le logement et les locataires.</p>
</div>
</div>
</div>
<div class="form-grid">
<mat-form-field appearance="outline"
><mat-label>Appartement</mat-label
><mat-select
name="apartmentId"
[ngModel]="apartmentId()"
(ngModelChange)="apartmentId.set($event)"
required
><mat-option value="">Sélectionnez un appartement</mat-option>
@for (apartment of apartments(); track apartment.id) {
<mat-option [value]="apartment.id">{{ apartment.name }}</mat-option>
}
</mat-select></mat-form-field
>
<mat-form-field appearance="outline"
><mat-label>Locataires</mat-label
><mat-select
name="tenantIds"
[ngModel]="tenantIds()"
(ngModelChange)="tenantIds.set($event)"
multiple
required
<div class="form-grid">
<mat-form-field appearance="outline"
><mat-label>Appartement</mat-label
><mat-select
name="apartmentId"
[ngModel]="apartmentId()"
(ngModelChange)="apartmentId.set($event)"
required
><mat-option value="">Sélectionnez un appartement</mat-option>
@for (apartment of apartments(); track apartment.id) {
<mat-option [value]="apartment.id">{{ apartment.name }}</mat-option>
}
</mat-select></mat-form-field
>
@for (tenant of tenants(); track tenant.id) {
<mat-option [value]="tenant.id"
>{{ tenant.firstName }} {{ tenant.lastName }}</mat-option
>
}
</mat-select></mat-form-field
>
</div>
</section>
<section class="form-section">
<mat-form-field appearance="outline"
><mat-label>Locataires</mat-label
><mat-select
name="tenantIds"
[ngModel]="tenantIds()"
(ngModelChange)="tenantIds.set($event)"
multiple
required
>
@for (tenant of tenants(); track tenant.id) {
<mat-option [value]="tenant.id"
>{{ tenant.firstName }} {{ tenant.lastName }}</mat-option
>
}
</mat-select></mat-form-field
>
</div>
</section>
<section class="form-section">
<div class="section-heading">
<span>02</span>
<div>
<h2>Conditions</h2>
<p>Définissez la période et les montants mensuels.</p>
</div>
</div>
<div class="form-grid three-columns">
<mat-form-field appearance="outline"
><mat-label>Date de début</mat-label
><input
matInput
[matDatepicker]="startDatePicker"
name="startDate"
[ngModel]="startDate()"
(ngModelChange)="startDate.set($event)"
required /><mat-datepicker-toggle
matIconSuffix
[for]="startDatePicker" /><mat-datepicker #startDatePicker
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Date de fin</mat-label
><input
matInput
[matDatepicker]="endDatePicker"
name="endDate"
[ngModel]="endDate()"
(ngModelChange)="endDate.set($event)"
required /><mat-datepicker-toggle
matIconSuffix
[for]="endDatePicker" /><mat-datepicker #endDatePicker
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Loyer mensuel</mat-label
><input
matInput
type="number"
name="monthlyRent"
[ngModel]="monthlyRent()"
(ngModelChange)="monthlyRent.set($event)"
min="1"
required
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline"
><mat-label>Charges mensuelles</mat-label
><input
matInput
type="number"
name="monthlyCharges"
[ngModel]="monthlyCharges()"
(ngModelChange)="monthlyCharges.set($event)"
min="0"
required
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline"
><mat-label>Dépôt de garantie</mat-label
><input
matInput
type="number"
name="securityDeposit"
[ngModel]="securityDeposit()"
(ngModelChange)="securityDeposit.set($event)"
min="0"
required
/><span matTextSuffix>€</span></mat-form-field
>
</div>
</section>
</fieldset>
</form>
<fieldset [disabled]="saving()" [attr.inert]="saving() ? '' : null">
<section class="guarantor-section">
<div class="section-heading">
<span>02</span>
<span>03</span>
<div>
<h2>Conditions</h2>
<p>Définissez la période et les montants mensuels.</p>
<h2>Garants</h2>
<p>Ajoutez un ou plusieurs contacts de garantie.</p>
</div>
</div>
<div class="form-grid three-columns">
<mat-form-field appearance="outline"
><mat-label>Date de début</mat-label
><input
matInput
[matDatepicker]="startDatePicker"
name="startDate"
[ngModel]="startDate()"
(ngModelChange)="startDate.set($event)"
required /><mat-datepicker-toggle
matIconSuffix
[for]="startDatePicker" /><mat-datepicker #startDatePicker
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Date de fin</mat-label
><input
matInput
[matDatepicker]="endDatePicker"
name="endDate"
[ngModel]="endDate()"
(ngModelChange)="endDate.set($event)"
required /><mat-datepicker-toggle matIconSuffix [for]="endDatePicker" /><mat-datepicker
#endDatePicker
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Loyer mensuel</mat-label
><input
matInput
type="number"
name="monthlyRent"
[ngModel]="monthlyRent()"
(ngModelChange)="monthlyRent.set($event)"
min="1"
required
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline"
><mat-label>Charges mensuelles</mat-label
><input
matInput
type="number"
name="monthlyCharges"
[ngModel]="monthlyCharges()"
(ngModelChange)="monthlyCharges.set($event)"
min="0"
required
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline"
><mat-label>Dépôt de garantie</mat-label
><input
matInput
type="number"
name="securityDeposit"
[ngModel]="securityDeposit()"
(ngModelChange)="securityDeposit.set($event)"
min="0"
required
/><span matTextSuffix>€</span></mat-form-field
>
</div>
@for (guarantor of guarantors(); track $index) {
<div class="guarantor-chip">
<span>{{ guarantor.firstName }} {{ guarantor.lastName }}</span
><button mat-button type="button" (click)="removeGuarantor($index)">Retirer</button>
</div>
}
<app-guarantor-form (submitted)="addGuarantor($event)" />
</section>
</form>
<section class="guarantor-section">
<div class="section-heading">
<span>03</span>
<div>
<h2>Garants</h2>
<p>Ajoutez un ou plusieurs contacts de garantie.</p>
</div>
</div>
@for (guarantor of guarantors(); track $index) {
<div class="guarantor-chip">
<span>{{ guarantor.firstName }} {{ guarantor.lastName }}</span
><button mat-button type="button" (click)="removeGuarantor($index)">Retirer</button>
</div>
}
<app-guarantor-form (submitted)="addGuarantor($event)" />
</section>
</fieldset>
<div class="form-actions">
<button mat-button type="button" (click)="cancelled.emit()">Annuler</button
><button mat-flat-button type="submit" form="lease-details" class="primary-action">
Enregistrer le bail
<button mat-button type="button" [disabled]="saving()" (click)="cancelled.emit()">
Annuler</button
><button
mat-flat-button
type="submit"
form="lease-details"
class="primary-action"
[disabled]="saving()"
>
<span aria-live="polite">
@if (saving()) {
<mat-spinner diameter="18" aria-hidden="true" /> Enregistrement…
} @else {
Enregistrer le bail
}
</span>
</button>
</div>
</section>
@@ -5,6 +5,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Apartment } from '../../services/apartment.service';
import { Guarantor, Lease } from '../../services/lease.service';
import { Tenant } from '../../services/tenant.service';
@@ -19,6 +20,7 @@ import { GuarantorForm } from '../guarantor-form/guarantor-form';
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatProgressSpinnerModule,
GuarantorForm,
],
templateUrl: './lease-form.html',
@@ -27,6 +29,8 @@ import { GuarantorForm } from '../guarantor-form/guarantor-form';
export class LeaseForm {
readonly apartments = input.required<Apartment[]>();
readonly tenants = input.required<Tenant[]>();
readonly saving = input(false);
readonly saveError = input('');
readonly cancelled = output<void>();
readonly submitted = output<Lease>();
readonly apartmentId = signal('');
@@ -50,6 +54,7 @@ export class LeaseForm {
}
save(form: NgForm): void {
if (this.saving()) return;
if (form.invalid) {
form.control.markAllAsTouched();
return;