Components - Composants Svelte Réutilisables

Vue d’ensemble des composants Svelte interactifs de l’application.

Localisation : frontend/src/components/

Organisation

components/
├── dashboards/           # Dashboards spécifiques par rôle
│   ├── AdminDashboard.svelte
│   ├── SyndicDashboard.svelte
│   ├── AccountantDashboard.svelte
│   └── OwnerDashboard.svelte
├── admin/                # Composants admin
│   └── SeedManager.svelte
├── BuildingList.svelte   # Liste immeubles avec pagination
├── OwnerList.svelte      # Liste copropriétaires
├── UnitList.svelte       # Liste lots
├── ExpenseList.svelte    # Liste charges
├── MeetingList.svelte    # Liste assemblées
├── DocumentList.svelte   # Liste documents
├── OrganizationList.svelte  # Liste organisations (superadmin)
├── UserListAdmin.svelte  # Liste utilisateurs (admin)
├── LoginForm.svelte      # Formulaire authentification
├── Navigation.svelte     # Navigation principale
├── Pagination.svelte     # Composant pagination réutilisable
├── SyncStatus.svelte     # Indicateur online/offline
└── LanguageSelector.svelte  # Sélecteur de langue

Catégories de Composants

Dashboards (par Rôle)

AdminDashboard.svelte

Dashboard pour SUPERADMIN : gestion organisations, utilisateurs, abonnements.

Features :

  • Stats organisations actives

  • Liste utilisateurs récents

  • Gestion abonnements (cloud vs self-hosted)

  • Seed manager (données de test)

SyndicDashboard.svelte

Dashboard pour SYNDIC : gestion quotidienne copropriétés.

Features :

  • Stats immeubles gérés, copropriétaires, charges

  • Tâches urgentes (réparations, convocations AG)

  • Actions rapides (immeubles, owners, charges, assemblées)

  • Prochaine assemblée générale

Exemple Code :

<script lang="ts">
  import { authStore } from '../../stores/auth';
  $: user = $authStore.user;
</script>

<div>
  <h1>Bienvenue, {user?.firstName} 👋</h1>
  <div class="grid grid-cols-4 gap-6">
    <!-- Stats Cards -->
    <div class="bg-white shadow p-6">
      <span>Immeubles gérés</span>
      <p class="text-3xl">8</p>
    </div>
  </div>
</div>

AccountantDashboard.svelte

Dashboard pour ACCOUNTANT : consultation comptable, rapports financiers.

Features :

  • Vue consolidée charges toutes copropriétés

  • Rapports financiers (revenus, dépenses, balance)

  • Export comptable (PDF, Excel)

OwnerDashboard.svelte

Dashboard pour OWNER : consultation personnelle.

Features :

  • Mes lots (appartements, parkings, caves)

  • Mes charges à payer

  • Mes documents (PCN, PV assemblées)

  • Coordonnées syndic

Composants Listes

BuildingList.svelte

Liste immeubles avec pagination, création inline.

Features :

  • ✅ Pagination (20 items par défaut)

  • ✅ Formulaire création inline

  • ✅ Affichage cartes (nom, adresse, nombre lots)

  • ✅ Authentification JWT automatique

  • ✅ Gestion erreurs

Props :

Aucune prop, composant standalone.

Exemple :

<script lang="ts">
  import BuildingList from '../components/BuildingList.svelte';
</script>

<BuildingList />

OwnerList.svelte

Liste copropriétaires avec recherche.

Features :

  • Liste complète copropriétaires

  • Recherche par nom, email

  • Affichage lots possédés

  • Coordonnées contact (GDPR protected)

UnitList.svelte

Liste lots d’un immeuble.

Features :

  • Filtre par building_id

  • Affichage type (Apartment, Parking, Storage)

  • Quote-part millièmes

  • Assignation copropriétaire

ExpenseList.svelte

Liste charges avec filtres.

Features :

  • Filtre par immeuble

  • Filtre par statut (Pending, Paid, Overdue)

  • Catégories (Maintenance, Repair, Insurance, Utilities, Management)

  • Marquage “Payé”

  • Total charges en attente

MeetingList.svelte

Liste assemblées générales.

Features :

  • Filtres par immeuble

  • Statuts (Scheduled, Completed, Cancelled)

  • Téléchargement PV (PDF)

  • Ordre du jour

DocumentList.svelte

Liste documents partagés.

Features :

  • Filtres par type (PCN, Règlement, Contrat, Facture, Other)

  • Upload documents

  • Téléchargement

  • Prévisualisation PDF inline (iframe)

Composants Utilitaires

Pagination.svelte

Composant pagination réutilisable pour toutes les listes.

Props :

interface Props {
  currentPage: number;
  totalPages: number;
  totalItems: number;
  perPage: number;
  onPageChange: (page: number) => void;
}

Exemple :

<Pagination
  currentPage={currentPage}
  totalPages={totalPages}
  totalItems={totalItems}
  perPage={perPage}
  onPageChange={handlePageChange}
/>

Rendu :

[<] [1] [2] [3] ... [10] [>]
Affichage 21-40 sur 200 résultats

Navigation.svelte

Navigation principale avec détection rôle utilisateur.

Features :

  • Menu adapté au rôle (superadmin, syndic, accountant, owner)

  • Indicateur utilisateur connecté

  • Bouton logout

  • Sélecteur de langue

  • Badge notifications (future)

Exemple :

<script lang="ts">
  import { authStore } from '../stores/auth';
  import { UserRole } from '../lib/types';

  $: user = $authStore.user;
  $: isSyndic = user?.role === UserRole.SYNDIC;
</script>

<nav>
  <a href="/dashboard">Dashboard</a>
  {#if isSyndic}
    <a href="/buildings">Immeubles</a>
    <a href="/owners">Copropriétaires</a>
  {/if}
</nav>

SyncStatus.svelte

Indicateur statut connexion online/offline.

Features :

  • 🟢 En ligne / 🔴 Hors ligne

  • Bouton synchronisation manuelle

  • Badge modifications en attente

Exemple :

<script lang="ts">
  import { syncService } from '../lib/sync';
  import { localDB } from '../lib/db';
  import { onMount } from 'svelte';

  let isOnline = syncService.getOnlineStatus();
  let pendingCount = 0;

  async function updateStatus() {
    isOnline = syncService.getOnlineStatus();
    const queue = await localDB.getSyncQueue();
    pendingCount = queue.filter(item => !item.synced).length;
  }

  onMount(() => {
    const interval = setInterval(updateStatus, 1000);
    return () => clearInterval(interval);
  });
</script>

<div class="sync-status">
  {#if isOnline}
    <span class="text-green-500">🟢 En ligne</span>
  {:else}
    <span class="text-orange-500">🔴 Hors ligne</span>
  {/if}

  {#if pendingCount > 0}
    <span class="badge">{pendingCount} en attente</span>
  {/if}
</div>

LanguageSelector.svelte

Sélecteur de langue (nl, fr, de, en).

Features :

  • Dropdown avec drapeaux

  • Persistance localStorage

  • Mise à jour dynamique traductions

Exemple :

<script lang="ts">
  import { locale } from 'svelte-i18n';
  import { languages } from '../lib/i18n';

  function changeLanguage(code: string) {
    $locale = code;
    localStorage.setItem('koprogo_locale', code);
  }
</script>

<select bind:value={$locale} on:change={(e) => changeLanguage(e.target.value)}>
  {#each languages as lang}
    <option value={lang.code}>
      {lang.flag} {lang.name}
    </option>
  {/each}
</select>

Composants Formulaires

LoginForm.svelte

Formulaire authentification JWT.

Features :

  • Email + Password

  • Gestion erreurs (401, 500)

  • Stockage token localStorage

  • Redirection après login

  • Initialisation SyncService

Exemple :

<script lang="ts">
  import { api } from '../lib/api';
  import { syncService } from '../lib/sync';
  import { authStore } from '../stores/auth';

  let email = '';
  let password = '';
  let error = '';

  async function handleLogin() {
    try {
      const response = await api.post('/auth/login', {
        email,
        password
      });

      const { token, user } = response;

      localStorage.setItem('koprogo_token', token);
      authStore.setUser(user);

      await syncService.initialize(token);

      window.location.href = '/dashboard';
    } catch (e) {
      error = e instanceof Error ? e.message : 'Erreur authentification';
    }
  }
</script>

<form on:submit|preventDefault={handleLogin}>
  <input type="email" bind:value={email} required />
  <input type="password" bind:value={password} required />
  <button type="submit">Se connecter</button>
  {#if error}<p class="error">{error}</p>{/if}
</form>

Composants Admin

SeedManager.svelte

Composant génération données de test (dev/staging).

Features :

  • Seed organisations + utilisateurs + immeubles

  • Seed complet (all entities)

  • Reset database

  • Logs generation

⚠️ Seulement pour environnements non-production !

OrganizationList.svelte

Liste organisations (multi-tenant).

Features :

  • CRUD organisations (superadmin only)

  • Stats utilisateurs par organisation

  • Désactivation/Activation compte

UserListAdmin.svelte

Liste utilisateurs plateforme.

Features :

  • CRUD utilisateurs (superadmin only)

  • Changement rôle

  • Reset password

  • Blocage compte

Patterns d’Utilisation

Hydration Astro

Dans les pages Astro, charger composants Svelte avec directives client:

---
import BuildingList from '../components/BuildingList.svelte';
---
<Layout>
  <!-- client:load : Hydrate immédiatement -->
  <BuildingList client:load />

  <!-- client:idle : Hydrate après chargement initial -->
  <SyncStatus client:idle />

  <!-- client:visible : Hydrate quand visible viewport -->
  <ExpenseList client:visible />
</Layout>

Communication Parent-Enfant

Props Down :

<!-- Parent -->
<BuildingCard building={selectedBuilding} />

<!-- Enfant -->
<script lang="ts">
  export let building: Building;
</script>

Events Up :

<!-- Enfant -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function handleClick() {
    dispatch('select', { building });
  }
</script>

<!-- Parent -->
<BuildingCard on:select={handleSelect} />

Stores Partagés

Pour état global (auth, preferences) :

<script lang="ts">
  import { authStore } from '../stores/auth';

  $: user = $authStore.user;
  $: isLoggedIn = $authStore.isLoggedIn;
</script>

{#if isLoggedIn}
  <p>Bienvenue {user.firstName}</p>
{/if}

Styling avec Tailwind CSS

Tous les composants utilisent Tailwind CSS :

<div class="bg-white rounded-lg shadow p-6 hover:shadow-md transition">
  <h3 class="text-lg font-semibold text-gray-900">{building.name}</h3>
  <p class="text-gray-600 text-sm">📍 {building.address}</p>
</div>

Classes Personnalisées :

/* frontend/src/styles/global.css */
.btn-primary {
  @apply bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700;
}

Tests Composants

// tests/unit/BuildingList.test.ts
import { render, screen } from '@testing-library/svelte';
import { vi } from 'vitest';
import BuildingList from '../src/components/BuildingList.svelte';

vi.mock('../src/lib/api', () => ({
  api: {
    get: vi.fn(() => Promise.resolve({
      data: [{ id: '1', name: 'Test Building' }],
      pagination: { current_page: 1, total_pages: 1 }
    }))
  }
}));

describe('BuildingList', () => {
  it('should render buildings', async () => {
    render(BuildingList);

    await screen.findByText('Test Building');
    expect(screen.getByText('Test Building')).toBeInTheDocument();
  });
});

Accessibilité (a11y)

Bonnes pratiques :

<!-- Labels pour inputs -->
<label for="building-name">Nom de l'immeuble</label>
<input id="building-name" type="text" />

<!-- Attributs ARIA -->
<button aria-label="Fermer" on:click={close}>
  <span aria-hidden="true">×</span>
</button>

<!-- Navigation clavier -->
<div role="menu" on:keydown={handleKeydown}>
  <button role="menuitem">Option 1</button>
</div>

Performance

Lazy Loading :

<script lang="ts">
  import { onMount } from 'svelte';

  let HeavyComponent;

  onMount(async () => {
    HeavyComponent = (await import('./HeavyComponent.svelte')).default;
  });
</script>

{#if HeavyComponent}
  <svelte:component this={HeavyComponent} />
{/if}

Virtual Scrolling :

Pour listes > 1000 items, utiliser svelte-virtual-list.

Références