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

@@ -34,6 +34,16 @@
display: grid;
gap: 16px;
}
.content-region > mat-progress-bar,
.refresh-error {
margin-bottom: 16px;
}
.skeleton-card {
padding: 20px;
border: 1px solid var(--border);
border-radius: 22px;
background: var(--surface);
}
@media (min-width: 700px) {
.page-heading {
display: flex;
@@ -10,34 +10,82 @@
<span aria-hidden="true">+</span> Ajouter un locataire
</button>
</header>
@if (workspace.tenants().length === 0) {
<article class="empty-state">
<p class="eyebrow">Premier dossier</p>
<h2>Aucun locataire enregistré</h2>
<p>Centralisez ici les coordonnées et pièces jointes utiles de vos locataires.</p>
<button mat-stroked-button type="button" (click)="openAddForm()">
Ajouter un locataire
</button>
</article>
} @else {
<div class="tenant-grid">
@for (tenant of workspace.tenants(); track tenant.id) {
<app-tenant-card
[tenant]="tenant"
[attachmentError]="workspace.tenantAttachmentError()"
(editRequested)="openEditForm($event)"
(deleteRequested)="workspace.deleteTenant($event)"
(attachmentUploadRequested)="workspace.uploadTenantAttachment($event.tenantId, $event.file)"
(attachmentDownloadRequested)="workspace.downloadTenantAttachment($event.tenantId, $event.attachment)"
/>
<div class="content-region" [attr.aria-busy]="workspace.tenantLoadState().loading">
@if (!workspace.tenantLoadState().loaded && workspace.tenantLoadState().loading) {
<div class="tenant-grid" aria-label="Chargement des locataires">
@for (placeholder of [1, 2, 3]; track placeholder) {
<article class="skeleton-card" aria-hidden="true">
<span class="skeleton-line skeleton-line-short"></span>
<span class="skeleton-line skeleton-line-title"></span>
<span class="skeleton-line"></span>
<span class="skeleton-line skeleton-line-medium"></span>
</article>
}
</div>
} @else if (!workspace.tenantLoadState().loaded && workspace.tenantLoadState().error) {
<article class="load-error" role="alert">
<h2>Chargement impossible</h2>
<p>{{ workspace.tenantLoadState().error }}</p>
<button mat-stroked-button type="button" (click)="workspace.loadTenants()">
Réessayer
</button>
</article>
} @else {
@if (workspace.tenantLoadState().loading) {
<mat-progress-bar mode="indeterminate" aria-label="Actualisation des locataires" />
}
</div>
}
@if (workspace.tenantLoadState().error) {
<div class="refresh-error" role="alert">
<p>{{ workspace.tenantLoadState().error }}</p>
<button mat-button type="button" (click)="workspace.loadTenants()">Réessayer</button>
</div>
}
@if (workspace.tenants().length === 0) {
<article class="empty-state">
<p class="eyebrow">Premier dossier</p>
<h2>Aucun locataire enregistré</h2>
<p>Centralisez ici les coordonnées et pièces jointes utiles de vos locataires.</p>
<button mat-stroked-button type="button" (click)="openAddForm()">
Ajouter un locataire
</button>
</article>
} @else {
<div class="tenant-grid">
@for (tenant of workspace.tenants(); track tenant.id) {
<app-tenant-card
[tenant]="tenant"
[attachmentError]="
tenant.id ? (workspace.tenantAttachmentErrors()[tenant.id] ?? '') : ''
"
[uploading]="workspace.uploadingTenantIds().has(tenant.id!)"
[uploadProgress]="
tenant.id ? (workspace.tenantUploadProgress()[tenant.id] ?? null) : null
"
[deleting]="!!tenant.id && workspace.deletingTenantIds().has(tenant.id)"
[deleteError]="tenant.id ? (workspace.tenantDeleteErrors()[tenant.id] ?? '') : ''"
[downloadingAttachmentIds]="workspace.downloadingAttachmentIds()"
[attachmentDownloadErrors]="workspace.attachmentDownloadErrors()"
(editRequested)="openEditForm($event)"
(deleteRequested)="workspace.deleteTenant($event)"
(attachmentUploadRequested)="
workspace.uploadTenantAttachment($event.tenantId, $event.file)
"
(attachmentDownloadRequested)="
workspace.downloadTenantAttachment($event.tenantId, $event.attachment)
"
/>
}
</div>
}
}
</div>
</section>
} @else {
<app-tenant-form
[tenant]="editingTenant()"
[saving]="workspace.tenantSaving()"
[saveError]="workspace.tenantSaveError()"
(cancelled)="closeForm()"
(submitted)="workspace.saveTenant($event.tenant, editingTenant()?.id); closeForm()"
(submitted)="saveTenant($event)"
/>
}
@@ -1,9 +1,19 @@
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { TenantsPage } from './tenants-page';
import { WorkspaceService } from '../../services/workspace.service';
describe('TenantsPage', () => {
beforeEach(() => TestBed.configureTestingModule({ providers: [{ provide: WorkspaceService, useValue: { loadTenants: () => undefined } }] }));
beforeEach(() =>
TestBed.configureTestingModule({
providers: [
{
provide: WorkspaceService,
useValue: { loadTenants: () => undefined, tenantSaveError: signal('') },
},
],
}),
);
it('opens and closes the focused tenant form module', () => {
const fixture = TestBed.createComponent(TenantsPage);
@@ -1,13 +1,14 @@
import { Component, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { Tenant } from '../../services/tenant.service';
import { WorkspaceService } from '../../services/workspace.service';
import { TenantCard } from '../tenant-card/tenant-card';
import { TenantForm } from '../tenant-form/tenant-form';
import { TenantForm, TenantSaveRequest } from '../tenant-form/tenant-form';
@Component({
selector: 'app-tenants-page',
imports: [MatButtonModule, TenantCard, TenantForm],
imports: [MatButtonModule, MatProgressBarModule, TenantCard, TenantForm],
templateUrl: './tenants-page.html',
styleUrl: './tenants-page.css',
})
@@ -17,13 +18,17 @@ export class TenantsPage {
readonly showForm = signal(false);
readonly editingTenant = signal<Tenant | null>(null);
constructor() { this.workspace.loadTenants(); }
constructor() {
this.workspace.loadTenants();
}
openAddForm(): void {
this.workspace.tenantSaveError.set('');
this.editingTenant.set(null);
this.showForm.set(true);
}
openEditForm(tenant: Tenant): void {
this.workspace.tenantSaveError.set('');
this.editingTenant.set(tenant);
this.showForm.set(true);
}
@@ -31,4 +36,7 @@ export class TenantsPage {
this.showForm.set(false);
this.editingTenant.set(null);
}
saveTenant(request: TenantSaveRequest): void {
this.workspace.saveTenant(request.tenant, request.tenantId, () => this.closeForm());
}
}