Document de Conception — Task Management System (Frontend)

1. Introduction et Choix Architectural

Vue d’ensemble

Le frontend du Task Management System est une application web monopage (SPA) construite selon l’approche Jamstack (JavaScript, APIs, Markup). L’interface permet aux utilisateurs de gérer des tâches, des projets et des équipes via une communication exclusive avec l’API RESTful du backend.

L’application est entièrement découplée du backend : vite build produit des fichiers statiques purs (HTML, JS, CSS) déployables sur n’importe quel CDN ou serveur de fichiers statiques. Zéro serveur côté frontend — toute l’interactivité est gérée par JavaScript côté client.

Stack technique

Technologie Rôle Justification
React 18+ Bibliothèque UI Écosystème mature, composants réutilisables, hooks
TypeScript Typage statique Sécurité du code, autocomplétion, types partagés avec le backend
Vite Bundler / Dev server Build rapide, HMR instantané, configuration minimale
Shadcn/UI (Radix UI) Composants UI Accessibilité native (WAI-ARIA), personnalisable, non-opinionated
TanStack Query Data fetching / Cache Cache intelligent, revalidation automatique, gestion des états serveur
Zustand State management client Léger, API simple, pas de boilerplate
React Router Routing Standard de facto pour le routing SPA React
Tailwind CSS Styling Utility-first, responsive design intégré, cohérence visuelle
Playwright Tests E2E Tests cross-browser, API puissante, recommandé par les standards projet

Principes Jamstack appliqués

  1. J — JavaScript : React gère toute l’interactivité côté client
  2. A — APIs : TanStack Query structure la communication avec le backend REST
  3. M — Markup : Vite produit le HTML statique initial, React hydrate le DOM
  4. Découplage total : Le frontend ne connaît que les URLs de l’API, aucune dépendance serveur
  5. Déploiement statique : vite build → dossier dist/ → CDN

2. Architecture Globale

Diagramme d’architecture

graph TD
    subgraph "Frontend (SPA Statique)"
        Router["React Router\n(Routes & Navigation)"]
        Pages["Pages\n(TasksPage, ProjectsPage, etc.)"]
        Components["Composants UI\n(Shadcn/UI + Custom)"]
        Hooks["Hooks TanStack Query\n(useTasksQuery, etc.)"]
        Stores["Zustand Stores\n(authStore, uiStore)"]
        ApiClient["Client API\n(fetch + intercepteurs)"]
        Validators["Validation Client\n(Zod schemas)"]
    end

    subgraph "Backend (API REST)"
        API["API RESTful\nExpress + PostgreSQL"]
    end

    CDN["CDN / Serveur Statique\n(fichiers HTML, JS, CSS)"]

    CDN -->|"Sert les fichiers statiques"| Router
    Router --> Pages
    Pages --> Components
    Pages --> Hooks
    Pages --> Stores
    Hooks --> ApiClient
    ApiClient -->|"HTTP (JSON)"| API
    Components --> Validators

Diagramme de flux de données

sequenceDiagram
    participant U as Utilisateur
    participant C as Composant React
    participant TQ as TanStack Query
    participant Z as Zustand Store
    participant API as Backend API

    U->>C: Interaction (clic, saisie)
    C->>Z: Lecture/écriture state local
    C->>TQ: Appel hook (useQuery/useMutation)
    TQ->>API: Requête HTTP (GET/POST/PATCH/DELETE)
    API-->>TQ: Réponse JSON
    TQ-->>TQ: Mise à jour du cache
    TQ-->>C: Données mises à jour
    C-->>U: Re-rendu de l'interface

Flux d’authentification (Release 2+ — Keycloak OIDC)

sequenceDiagram
    participant U as Utilisateur
    participant F as Frontend SPA
    participant K as Keycloak (auth.localhost)
    participant API as Backend API

    U->>F: Navigation vers /tasks
    F->>F: Pas de token → redirection /login
    F->>K: Redirect vers /realms/task-management/auth (PKCE)
    K-->>U: Page de connexion Keycloak
    U->>K: Email + mot de passe
    K-->>F: Redirect avec authorization_code
    F->>K: POST /token (code + code_verifier)
    K-->>F: access_token (JWT RS256) + refresh_token
    F->>API: POST /api/auth/login { keycloak_token }
    API-->>API: Valider JWT via JWKS Keycloak
    API-->>F: 200 { user, session_token }
    F->>F: Stocker session_token dans localStorage
    F-->>U: Redirection vers /tasks

3. Structure des Dossiers

frontend/
├── public/
│   └── favicon.ico
├── src/
│   ├── api/
│   │   ├── client.ts              # Client HTTP (fetch wrapper + intercepteurs)
│   │   ├── tasks.ts               # Fonctions API pour les tâches
│   │   ├── projects.ts            # Fonctions API pour les projets
│   │   ├── users.ts               # Fonctions API pour les utilisateurs
│   │   ├── teams.ts               # Fonctions API pour les équipes
│   │   ├── assignments.ts         # Fonctions API pour les attributions
│   │   └── notifications.ts       # Fonctions API pour les notifications
│   ├── components/
│   │   ├── ui/                    # Composants Shadcn/UI (Button, Input, Dialog, etc.)
│   │   ├── layout/
│   │   │   ├── AppLayout.tsx      # Layout principal (sidebar + header + contenu)
│   │   │   ├── Sidebar.tsx        # Navigation latérale
│   │   │   └── Header.tsx         # Barre supérieure (user info, notifications)
│   │   ├── tasks/
│   │   │   ├── TaskList.tsx        # Liste des tâches (tableau/cartes)
│   │   │   ├── TaskCard.tsx        # Carte individuelle d'une tâche
│   │   │   ├── TaskForm.tsx        # Formulaire création/édition de tâche
│   │   │   ├── TaskStatusBadge.tsx # Badge de statut coloré
│   │   │   ├── TaskPriorityBadge.tsx # Badge de priorité
│   │   │   └── TaskFilters.tsx     # Filtres et tri des tâches
│   │   ├── projects/
│   │   │   ├── ProjectList.tsx     # Liste des projets
│   │   │   ├── ProjectCard.tsx     # Carte individuelle d'un projet
│   │   │   └── ProjectForm.tsx     # Formulaire création/édition de projet
│   │   ├── teams/
│   │   │   ├── TeamList.tsx        # Liste des équipes
│   │   │   ├── TeamCard.tsx        # Carte individuelle d'une équipe
│   │   │   └── TeamForm.tsx        # Formulaire création/édition d'équipe
│   │   ├── dashboard/
│   │   │   ├── StatusCounters.tsx  # Compteurs par statut
│   │   │   ├── CompletionRate.tsx  # Taux de complétion
│   │   │   └── OverdueTasks.tsx    # Liste des tâches en retard
│   │   └── common/
│   │       ├── ProtectedRoute.tsx  # Guard de route basé sur les rôles
│   │       ├── ErrorBoundary.tsx   # Gestion des erreurs React
│   │       ├── LoadingSpinner.tsx  # Indicateur de chargement
│   │       └── EmptyState.tsx      # État vide (aucune donnée)
│   ├── hooks/
│   │   ├── useTasks.ts            # Hooks TanStack Query pour les tâches
│   │   ├── useProjects.ts         # Hooks TanStack Query pour les projets
│   │   ├── useUsers.ts            # Hooks TanStack Query pour les utilisateurs
│   │   ├── useTeams.ts            # Hooks TanStack Query pour les équipes
│   │   ├── useAssignments.ts      # Hooks TanStack Query pour les attributions
│   │   ├── useNotifications.ts    # Hooks TanStack Query pour les notifications
│   │   └── useDashboard.ts        # Hooks TanStack Query pour le tableau de bord
│   ├── stores/
│   │   ├── authStore.ts           # State d'authentification (user, token, rôles)
│   │   └── uiStore.ts             # State UI (sidebar ouverte, thème, filtres actifs)
│   ├── types/
│   │   └── index.ts               # Types frontend (miroir des types backend + types UI)
│   ├── lib/
│   │   ├── validators.ts          # Schémas Zod pour la validation côté client
│   │   ├── permissions.ts         # Utilitaires de vérification des permissions
│   │   ├── date-utils.ts          # Utilitaires de formatage de dates
│   │   └── constants.ts           # Constantes (statuts, priorités, rôles, URLs API)
│   ├── pages/
│   │   ├── LoginPage.tsx          # Page de connexion
│   │   ├── TasksPage.tsx          # Page liste des tâches
│   │   ├── TaskDetailPage.tsx     # Page détail d'une tâche
│   │   ├── ProjectsPage.tsx       # Page liste des projets
│   │   ├── ProjectDetailPage.tsx  # Page détail d'un projet (avec ses tâches)
│   │   ├── TeamsPage.tsx          # Page gestion des équipes
│   │   ├── DashboardPage.tsx      # Page tableau de bord
│   │   ├── NotificationsPage.tsx  # Page notifications
│   │   └── NotFoundPage.tsx       # Page 404
│   ├── router.tsx                 # Configuration React Router
│   ├── App.tsx                    # Composant racine (providers)
│   └── main.tsx                   # Point d'entrée (ReactDOM.createRoot)
├── tests/
│   ├── unit/
│   │   ├── lib/               # Tests unitaires TDD pour validators, permissions, utilitaires
│   │   └── stores/            # Tests unitaires TDD pour les stores Zustand
│   └── e2e/
│       ├── tasks.spec.ts              # Tests E2E tâches
│       ├── projects.spec.ts           # Tests E2E projets
│       ├── auth.spec.ts               # Tests E2E authentification
│       ├── dashboard.spec.ts          # Tests E2E tableau de bord
│       └── fixtures/
│           └── test-data.ts           # Données de test partagées
├── index.html
├── vite.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── playwright.config.ts
└── package.json

4. Couche API (Data Fetching)

Client HTTP

Le client HTTP centralise la configuration des requêtes vers le backend. Il utilise l’API native fetch avec des intercepteurs pour l’authentification et la gestion des erreurs.

// src/api/client.ts

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';

/**
 * Client HTTP centralisé pour les appels API.
 * Ajoute automatiquement le token JWT et gère les erreurs.
 */
async function apiClient<T>(
  endpoint: string,
  options: RequestInit = {}
): Promise<T> {
  const token = useAuthStore.getState().token;

  const response = await fetch(`${API_BASE_URL}${endpoint}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    },
  });

  if (!response.ok) {
    const error: ApiError = await response.json();
    throw new ApiClientError(error.error, error.code, response.status, error.details);
  }

  // HTTP 204 No Content
  if (response.status === 204) return undefined as T;

  return response.json();
}

Hooks TanStack Query — Tâches

// src/hooks/useTasks.ts

/** Récupère la liste des tâches avec filtres optionnels */
function useTasksQuery(filters?: TaskFilters) {
  return useQuery({
    queryKey: ['tasks', filters],
    queryFn: () => fetchTasks(filters),
    staleTime: 30_000, // 30 secondes avant revalidation
  });
}

/** Récupère une tâche par son ID */
function useTaskQuery(taskId: string) {
  return useQuery({
    queryKey: ['tasks', taskId],
    queryFn: () => fetchTask(taskId),
    enabled: !!taskId,
  });
}

/** Crée une nouvelle tâche */
function useCreateTaskMutation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateTaskInput) => createTask(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      queryClient.invalidateQueries({ queryKey: ['dashboard'] });
    },
  });
}

/** Met à jour une tâche existante */
function useUpdateTaskMutation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateTaskInput }) =>
      updateTask(id, data),
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      queryClient.invalidateQueries({ queryKey: ['tasks', id] });
      queryClient.invalidateQueries({ queryKey: ['dashboard'] });
    },
  });
}

/** Supprime une tâche */
function useDeleteTaskMutation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (taskId: string) => deleteTask(taskId),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      queryClient.invalidateQueries({ queryKey: ['dashboard'] });
    },
  });
}

Hooks TanStack Query — Projets

// src/hooks/useProjects.ts

/** Récupère la liste des projets */
function useProjectsQuery() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    staleTime: 60_000,
  });
}

/** Récupère les tâches d'un projet spécifique */
function useProjectTasksQuery(projectId: string) {
  return useQuery({
    queryKey: ['projects', projectId, 'tasks'],
    queryFn: () => fetchProjectTasks(projectId),
    enabled: !!projectId,
  });
}

/** Crée un nouveau projet */
function useCreateProjectMutation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateProjectInput) => createProject(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });
}

Fonctions API

// src/api/tasks.ts

/** GET /api/tasks — Liste des tâches avec filtres optionnels (paginé) */
async function fetchTasks(filters?: TaskFilters): Promise<PaginatedResponse<Task>> {
  const params = new URLSearchParams();
  if (filters?.status) params.set('status', filters.status);
  if (filters?.priority) params.set('priority', filters.priority);
  if (filters?.projectId) params.set('projectId', filters.projectId);
  if (filters?.assignedTo) params.set('assignedTo', filters.assignedTo);
  if (filters?.sortBy) params.set('sortBy', filters.sortBy);
  if (filters?.page) params.set('page', String(filters.page));
  if (filters?.limit) params.set('limit', String(filters.limit));

  const query = params.toString();
  return apiClient<PaginatedResponse<Task>>(`/tasks${query ? `?${query}` : ''}`);
}

/** POST /api/tasks — Création d'une tâche */
async function createTask(data: CreateTaskInput): Promise<ApiResponseWithWarnings<Task>> {
  return apiClient<ApiResponseWithWarnings<Task>>('/tasks', {
    method: 'POST',
    body: JSON.stringify(data),
  });
}

/** PATCH /api/tasks/:id — Mise à jour d'une tâche */
async function updateTask(id: string, data: UpdateTaskInput): Promise<Task> {
  return apiClient<Task>(`/tasks/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(data),
  });
}

/** DELETE /api/tasks/:id — Suppression d'une tâche */
async function deleteTask(id: string): Promise<void> {
  return apiClient<void>(`/tasks/${id}`, { method: 'DELETE' });
}

/** GET /api/tasks/:id — Détail d'une tâche */
async function fetchTask(id: string): Promise<Task> {
  return apiClient<Task>(`/tasks/${id}`);
}
// src/api/projects.ts

/** GET /api/projects — Liste des projets */
async function fetchProjects(): Promise<Project[]> {
  return apiClient<Project[]>('/projects');
}

/** GET /api/projects/:id/tasks — Tâches d'un projet */
async function fetchProjectTasks(projectId: string): Promise<Task[]> {
  return apiClient<Task[]>(`/projects/${projectId}/tasks`);
}

/** POST /api/projects — Création d'un projet */
async function createProject(data: CreateProjectInput): Promise<Project> {
  return apiClient<Project>('/projects', {
    method: 'POST',
    body: JSON.stringify(data),
  });
}

Stratégie de cache TanStack Query

Ressource queryKey staleTime Invalidation
Liste des tâches (paginé) ['tasks', filters] 30s Après création/modification/suppression de tâche
Détail d’une tâche ['tasks', id] 30s Après modification de cette tâche
Tâches d’un projet (paginé) ['projects', id, 'tasks'] 30s Après modification de tâche dans ce projet
Liste des projets (paginé) ['projects'] 60s Après création/modification de projet
Tableau de bord ['dashboard', filters] 30s Après toute modification de tâche
Notifications ['notifications'] 15s Après marquage comme lue
Équipes ['teams'] 60s Après modification d’équipe

5. State Management (Zustand)

Auth Store

Le store d’authentification gère l’état de connexion de l’utilisateur, son token JWT et ses rôles. C’est le seul state véritablement « client » — tout le reste est du state serveur géré par TanStack Query.

// src/stores/authStore.ts

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;

  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  hasRole: (role: Role) => boolean;
  hasAnyRole: (roles: Role[]) => boolean;
  canEditTask: (task: Task) => boolean;
}

const useAuthStore = create<AuthState>((set, get) => ({
  user: null,
  token: localStorage.getItem('auth_token'),
  isAuthenticated: !!localStorage.getItem('auth_token'),

  login: async (email, password) => {
    const response = await apiClient<{ user: User; token: string }>('/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    });
    localStorage.setItem('auth_token', response.token);
    set({ user: response.user, token: response.token, isAuthenticated: true });
  },

  logout: () => {
    localStorage.removeItem('auth_token');
    set({ user: null, token: null, isAuthenticated: false });
  },

  hasRole: (role) => {
    const { user } = get();
    return user?.roles.includes(role) ?? false;
  },

  hasAnyRole: (roles) => {
    const { user } = get();
    return user?.roles.some((r) => roles.includes(r)) ?? false;
  },

  canEditTask: (task) => {
    const { user } = get();
    if (!user) return false;
    if (user.roles.includes('admin') || user.roles.includes('manager')) return true;
    if (user.roles.includes('developer')) return task.createdBy === user.id;
    return false; // viewer ne peut pas modifier
  },
}));

UI Store

Le store UI gère les préférences d’interface qui ne dépendent pas du serveur.

// src/stores/uiStore.ts

interface UIState {
  sidebarOpen: boolean;
  activeFilters: TaskFilters;
  sortBy: 'priority' | 'dueDate' | 'createdAt' | 'status';
  sortOrder: 'asc' | 'desc';

  toggleSidebar: () => void;
  setFilters: (filters: Partial<TaskFilters>) => void;
  resetFilters: () => void;
  setSortBy: (sortBy: UIState['sortBy']) => void;
  toggleSortOrder: () => void;
}

const useUIStore = create<UIState>((set) => ({
  sidebarOpen: true,
  activeFilters: {},
  sortBy: 'createdAt',
  sortOrder: 'desc',

  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
  setFilters: (filters) =>
    set((s) => ({ activeFilters: { ...s.activeFilters, ...filters } })),
  resetFilters: () => set({ activeFilters: {} }),
  setSortBy: (sortBy) => set({ sortBy }),
  toggleSortOrder: () =>
    set((s) => ({ sortOrder: s.sortOrder === 'asc' ? 'desc' : 'asc' })),
}));

Séparation State Serveur / State Client

graph LR
    subgraph "State Serveur (TanStack Query)"
        T["Tâches"]
        P["Projets"]
        Te["Équipes"]
        N["Notifications"]
        D["Dashboard"]
    end

    subgraph "State Client (Zustand)"
        A["Auth (user, token, rôles)"]
        UI["UI (sidebar, filtres, tri)"]
    end

    T -.->|"Cache + Revalidation"| API["Backend API"]
    P -.->|"Cache + Revalidation"| API
    Te -.->|"Cache + Revalidation"| API
    N -.->|"Cache + Revalidation"| API
    D -.->|"Cache + Revalidation"| API

    A -->|"localStorage"| Browser["Navigateur"]
    UI -->|"Mémoire"| Browser

6. Composants UI Principaux

Layout Principal

Le layout suit un modèle classique avec sidebar de navigation, header et zone de contenu principal.

graph TD
    subgraph "AppLayout"
        subgraph "Sidebar"
            Nav["Navigation\n• Tâches\n• Projets\n• Équipes\n• Dashboard\n• Notifications"]
        end
        subgraph "Zone Principale"
            Header["Header\n(Nom utilisateur, rôle, déconnexion, notifications)"]
            Content["Zone de Contenu\n(Pages via React Router Outlet)"]
        end
    end
// src/components/layout/AppLayout.tsx

/**
 * Layout principal de l'application.
 * Sidebar responsive (masquée sur mobile, toggle via hamburger).
 * Header avec informations utilisateur et badge de notifications.
 */
function AppLayout() {
  const { sidebarOpen, toggleSidebar } = useUIStore();
  const { user } = useAuthStore();

  return (
    <div className="flex h-screen bg-gray-50">
      <Sidebar open={sidebarOpen} onToggle={toggleSidebar} />
      <div className="flex flex-1 flex-col overflow-hidden">
        <Header user={user} onMenuToggle={toggleSidebar} />
        <main className="flex-1 overflow-y-auto p-6">
          <Outlet /> {/* React Router rend la page active ici */}
        </main>
      </div>
    </div>
  );
}

Composants Tâches

TaskList — Liste des tâches

// src/components/tasks/TaskList.tsx

/**
 * Affiche la liste des tâches sous forme de tableau responsive.
 * Supporte le tri par colonnes et le filtrage.
 * Affiche un état vide si aucune tâche ne correspond aux filtres.
 */
function TaskList() {
  const filters = useUIStore((s) => s.activeFilters);
  const { data: tasks, isLoading, error } = useTasksQuery(filters);

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorDisplay error={error} />;
  if (!tasks?.length) return <EmptyState message="Aucune tâche trouvée" />;

  return (
    <div className="space-y-4">
      <TaskFilters />
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
        {tasks.map((task) => (
          <TaskCard key={task.id} task={task} />
        ))}
      </div>
    </div>
  );
}

TaskCard — Carte d’une tâche

// src/components/tasks/TaskCard.tsx

/**
 * Carte individuelle affichant les informations clés d'une tâche :
 * description, statut, priorité, échéance, indicateur de retard.
 * Clic → navigation vers la page de détail.
 */
function TaskCard({ task }: { task: Task }) {
  const navigate = useNavigate();
  const { canEditTask } = useAuthStore();

  return (
    <Card
      className="cursor-pointer hover:shadow-md transition-shadow"
      onClick={() => navigate(`/tasks/${task.id}`)}
      role="article"
      aria-label={`Tâche: ${task.description}`}
    >
      <CardHeader className="flex flex-row items-center justify-between">
        <TaskPriorityBadge priority={task.priority} />
        <TaskStatusBadge status={task.status} />
      </CardHeader>
      <CardContent>
        <p className="text-sm text-gray-700 line-clamp-2">{task.description}</p>
        {task.dueDate && (
          <div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
            <CalendarIcon className="h-3 w-3" />
            <span className={task.isOverdue ? 'text-red-600 font-semibold' : ''}>
              {formatDate(task.dueDate)}
            </span>
            {task.isOverdue && (
              <span className="text-red-600 ml-1" role="status">En retard</span>
            )}
          </div>
        )}
      </CardContent>
    </Card>
  );
}

TaskStatusBadge — Badge de statut

// src/components/tasks/TaskStatusBadge.tsx

const STATUS_CONFIG: Record<Status, { label: string; className: string }> = {
  'todo':        { label: 'À faire',    className: 'bg-gray-100 text-gray-800' },
  'in-progress': { label: 'En cours',   className: 'bg-blue-100 text-blue-800' },
  'completed':   { label: 'Terminée',   className: 'bg-green-100 text-green-800' },
};

function TaskStatusBadge({ status }: { status: Status }) {
  const config = STATUS_CONFIG[status];
  return (
    <Badge className={config.className} aria-label={`Statut: ${config.label}`}>
      {config.label}
    </Badge>
  );
}

TaskPriorityBadge — Badge de priorité

// src/components/tasks/TaskPriorityBadge.tsx

const PRIORITY_CONFIG: Record<Priority, { label: string; className: string }> = {
  'low':    { label: 'Basse',   className: 'bg-slate-100 text-slate-700' },
  'medium': { label: 'Moyenne', className: 'bg-yellow-100 text-yellow-800' },
  'high':   { label: 'Haute',   className: 'bg-red-100 text-red-800' },
};

function TaskPriorityBadge({ priority }: { priority: Priority }) {
  const config = PRIORITY_CONFIG[priority];
  return (
    <Badge className={config.className} aria-label={`Priorité: ${config.label}`}>
      {config.label}
    </Badge>
  );
}

Composants Projets

ProjectCard — Carte d’un projet

// src/components/projects/ProjectCard.tsx

/**
 * Carte affichant un projet avec son nom, sa description,
 * et un compteur de tâches par statut.
 */
function ProjectCard({ project }: { project: Project }) {
  const navigate = useNavigate();
  const { data: tasks } = useProjectTasksQuery(project.id);

  const statusCounts = useMemo(() => {
    if (!tasks) return { todo: 0, 'in-progress': 0, completed: 0 };
    return tasks.reduce(
      (acc, t) => ({ ...acc, [t.status]: acc[t.status] + 1 }),
      { todo: 0, 'in-progress': 0, completed: 0 }
    );
  }, [tasks]);

  return (
    <Card
      className="cursor-pointer hover:shadow-md transition-shadow"
      onClick={() => navigate(`/projects/${project.id}`)}
      role="article"
      aria-label={`Projet: ${project.name}`}
    >
      <CardHeader>
        <CardTitle>{project.name}</CardTitle>
        {project.description && (
          <CardDescription>{project.description}</CardDescription>
        )}
      </CardHeader>
      <CardContent className="flex gap-3">
        <span className="text-xs text-gray-500">{statusCounts.todo} à faire</span>
        <span className="text-xs text-blue-500">{statusCounts['in-progress']} en cours</span>
        <span className="text-xs text-green-500">{statusCounts.completed} terminées</span>
      </CardContent>
    </Card>
  );
}

Composants Dashboard

StatusCounters — Compteurs par statut

// src/components/dashboard/StatusCounters.tsx

/**
 * Affiche trois cartes avec le nombre de tâches par statut.
 * Correspond à l'Exigence 11 — Tableau de bord de progression.
 */
function StatusCounters({ tasks }: { tasks: Task[] }) {
  const counts = useMemo(() => ({
    todo: tasks.filter((t) => t.status === 'todo').length,
    'in-progress': tasks.filter((t) => t.status === 'in-progress').length,
    completed: tasks.filter((t) => t.status === 'completed').length,
  }), [tasks]);

  return (
    <div className="grid grid-cols-1 gap-4 sm:grid-cols-3" role="region" aria-label="Compteurs de statut">
      <StatCard label="À faire" count={counts.todo} color="gray" />
      <StatCard label="En cours" count={counts['in-progress']} color="blue" />
      <StatCard label="Terminées" count={counts.completed} color="green" />
    </div>
  );
}

CompletionRate — Taux de complétion

// src/components/dashboard/CompletionRate.tsx

/**
 * Affiche le taux de complétion sous forme de barre de progression.
 * Calcul : (completed / total) * 100, arrondi à l'entier.
 * Correspond à l'Exigence 11.4.
 */
function CompletionRate({ tasks }: { tasks: Task[] }) {
  const rate = useMemo(() => {
    if (tasks.length === 0) return 0;
    const completed = tasks.filter((t) => t.status === 'completed').length;
    return Math.round((completed / tasks.length) * 100);
  }, [tasks]);

  return (
    <Card>
      <CardHeader>
        <CardTitle>Taux de complétion</CardTitle>
      </CardHeader>
      <CardContent>
        <div className="flex items-center gap-4">
          <Progress value={rate} aria-label={`${rate}% complété`} />
          <span className="text-2xl font-bold">{rate}%</span>
        </div>
      </CardContent>
    </Card>
  );
}

7. Routing (React Router)

Configuration des routes

// src/router.tsx

const router = createBrowserRouter([
  {
    path: '/login',
    element: <LoginPage />,
  },
  {
    path: '/',
    element: (
      <ProtectedRoute>
        <AppLayout />
      </ProtectedRoute>
    ),
    errorElement: <ErrorBoundary />,
    children: [
      { index: true, element: <Navigate to="/tasks" replace /> },
      { path: 'tasks', element: <TasksPage /> },
      { path: 'tasks/:taskId', element: <TaskDetailPage /> },
      { path: 'projects', element: <ProjectsPage /> },
      { path: 'projects/:projectId', element: <ProjectDetailPage /> },
      {
        path: 'teams',
        element: (
          <ProtectedRoute requiredRoles={['admin', 'manager']}>
            <TeamsPage />
          </ProtectedRoute>
        ),
      },
      {
        path: 'dashboard',
        element: (
          <ProtectedRoute requiredRoles={['admin', 'manager']}>
            <DashboardPage />
          </ProtectedRoute>
        ),
      },
      { path: 'notifications', element: <NotificationsPage /> },
    ],
  },
  { path: '*', element: <NotFoundPage /> },
]);

Carte des routes et permissions

Route Page Rôles autorisés Release
/login LoginPage Tous (non authentifié) MVP
/tasks TasksPage Tous (authentifié) MVP
/tasks/:taskId TaskDetailPage Tous (authentifié) MVP
/projects ProjectsPage Tous (authentifié) MVP
/projects/:projectId ProjectDetailPage Tous (authentifié) MVP
/teams TeamsPage admin, manager Release 2
/dashboard DashboardPage admin, manager Release 3
/notifications NotificationsPage Tous (authentifié) Release 3

Diagramme de navigation

graph TD
    Login["/login\nPage de connexion"]
    Tasks["/tasks\nListe des tâches"]
    TaskDetail["/tasks/:id\nDétail tâche"]
    Projects["/projects\nListe des projets"]
    ProjectDetail["/projects/:id\nDétail projet"]
    Teams["/teams\nGestion équipes"]
    Dashboard["/dashboard\nTableau de bord"]
    Notifs["/notifications\nNotifications"]
    NotFound["/*\nPage 404"]

    Login -->|"Authentification réussie"| Tasks
    Tasks -->|"Clic sur tâche"| TaskDetail
    Tasks -->|"Sidebar"| Projects
    Tasks -->|"Sidebar"| Teams
    Tasks -->|"Sidebar"| Dashboard
    Tasks -->|"Header"| Notifs
    Projects -->|"Clic sur projet"| ProjectDetail
    ProjectDetail -->|"Clic sur tâche"| TaskDetail
    TaskDetail -->|"Retour"| Tasks

8. Gestion des Formulaires et Validation Côté Client

Schémas de validation (Zod)

La validation côté client utilise Zod pour garantir la cohérence avec les règles métier avant l’envoi au backend. Cela offre un retour immédiat à l’utilisateur sans attendre la réponse serveur.

// src/lib/validators.ts

import { z } from 'zod';

/** Valeurs valides pour les énumérations du domaine */
const STATUS_VALUES = ['todo', 'in-progress', 'completed'] as const;
const PRIORITY_VALUES = ['low', 'medium', 'high'] as const;
const ROLE_VALUES = ['admin', 'manager', 'developer', 'viewer'] as const;

/** Schéma de création d'une tâche (Exigences 1, 7) */
const createTaskSchema = z.object({
  description: z
    .string()
    .trim()
    .min(1, 'La description est obligatoire')
    .max(250, 'La description ne peut pas dépasser 250 caractères'),
  priority: z.enum(PRIORITY_VALUES).optional().default('medium'),
  dueDate: z
    .string()
    .datetime({ message: "Format de date invalide (ISO 8601 attendu)" })
    .optional()
    .nullable(),
  projectId: z.string().uuid().optional().nullable(),
});

/** Schéma de mise à jour d'une tâche */
const updateTaskSchema = z.object({
  description: z
    .string()
    .trim()
    .min(1, 'La description est obligatoire')
    .max(250, 'La description ne peut pas dépasser 250 caractères')
    .optional(),
  status: z.enum(STATUS_VALUES, {
    errorMap: () => ({
      message: `Statut invalide. Valeurs acceptées : ${STATUS_VALUES.join(', ')}`,
    }),
  }).optional(),
  priority: z.enum(PRIORITY_VALUES, {
    errorMap: () => ({
      message: `Priorité invalide. Valeurs acceptées : ${PRIORITY_VALUES.join(', ')}`,
    }),
  }).optional(),
  dueDate: z.string().datetime().optional().nullable(),
});

/** Schéma de création d'un projet (Exigence 2) */
const createProjectSchema = z.object({
  name: z.string().trim().min(1, 'Le nom du projet est obligatoire'),
  description: z.string().trim().optional().nullable(),
  teamId: z.string().uuid().optional().nullable(),
});

/** Schéma de connexion */
const loginSchema = z.object({
  email: z.string().email('Adresse email invalide'),
  password: z.string().min(1, 'Le mot de passe est obligatoire'),
});

Formulaire de création de tâche

// src/components/tasks/TaskForm.tsx

/**
 * Formulaire de création/édition de tâche.
 * Validation côté client avec Zod avant soumission.
 * Affiche les erreurs inline sous chaque champ.
 * Gère l'avertissement pour les échéances dans le passé (Exigence 7.4).
 */
function TaskForm({ task, onClose }: { task?: Task; onClose: () => void }) {
  const createMutation = useCreateTaskMutation();
  const updateMutation = useUpdateTaskMutation();
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [warning, setWarning] = useState<string | null>(null);

  const isEditing = !!task;

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const data = Object.fromEntries(formData);

    // Validation côté client
    const schema = isEditing ? updateTaskSchema : createTaskSchema;
    const result = schema.safeParse(data);

    if (!result.success) {
      const fieldErrors: Record<string, string> = {};
      result.error.issues.forEach((issue) => {
        const field = issue.path[0] as string;
        fieldErrors[field] = issue.message;
      });
      setErrors(fieldErrors);
      return;
    }

    // Avertissement échéance passée
    if (result.data.dueDate && new Date(result.data.dueDate) < new Date()) {
      setWarning("Attention : l'échéance spécifiée est déjà dépassée.");
    }

    // Soumission
    if (isEditing) {
      updateMutation.mutate({ id: task.id, data: result.data }, { onSuccess: onClose });
    } else {
      createMutation.mutate(result.data, { onSuccess: onClose });
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4" noValidate>
      <div>
        <Label htmlFor="description">Description *</Label>
        <Textarea
          id="description"
          name="description"
          defaultValue={task?.description}
          maxLength={250}
          required
          aria-describedby={errors.description ? 'description-error' : undefined}
          aria-invalid={!!errors.description}
        />
        {errors.description && (
          <p id="description-error" className="text-sm text-red-600 mt-1" role="alert">
            {errors.description}
          </p>
        )}
      </div>

      <div>
        <Label htmlFor="priority">Priorité</Label>
        <Select name="priority" defaultValue={task?.priority ?? 'medium'}>
          <SelectTrigger>
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="low">Basse</SelectItem>
            <SelectItem value="medium">Moyenne</SelectItem>
            <SelectItem value="high">Haute</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div>
        <Label htmlFor="dueDate">Échéance</Label>
        <Input
          id="dueDate"
          name="dueDate"
          type="datetime-local"
          defaultValue={task?.dueDate ? formatDateForInput(task.dueDate) : undefined}
        />
        {warning && (
          <p className="text-sm text-yellow-600 mt-1" role="status">{warning}</p>
        )}
      </div>

      <div className="flex justify-end gap-2">
        <Button type="button" variant="outline" onClick={onClose}>
          Annuler
        </Button>
        <Button
          type="submit"
          disabled={createMutation.isPending || updateMutation.isPending}
        >
          {isEditing ? 'Mettre à jour' : 'Créer la tâche'}
        </Button>
      </div>
    </form>
  );
}

Gestion des erreurs API dans les formulaires

sequenceDiagram
    participant U as Utilisateur
    participant F as Formulaire
    participant Z as Zod Validation
    participant TQ as TanStack Query
    participant API as Backend API

    U->>F: Remplit et soumet le formulaire
    F->>Z: Validation côté client
    alt Validation échouée
        Z-->>F: Erreurs de validation
        F-->>U: Affiche erreurs inline (rouge)
    else Validation réussie
        Z-->>F: Données validées
        F->>TQ: mutation.mutate(data)
        TQ->>API: POST/PATCH requête
        alt Succès API
            API-->>TQ: 201/200 + données
            TQ-->>F: onSuccess callback
            F-->>U: Ferme le formulaire + toast succès
        else Erreur API (400)
            API-->>TQ: 400 + ApiError JSON
            TQ-->>F: onError callback
            F-->>U: Affiche message d'erreur serveur
        else Erreur API (403)
            API-->>TQ: 403 Forbidden
            TQ-->>F: onError callback
            F-->>U: "Vous n'avez pas les permissions nécessaires"
        end
    end

9. Authentification et Gestion des Rôles

Flux d’authentification

sequenceDiagram
    participant U as Utilisateur
    participant LP as LoginPage
    participant AS as AuthStore
    participant API as Backend API
    participant R as React Router

    U->>LP: Saisit email + mot de passe
    LP->>AS: login(email, password)
    AS->>API: POST /api/auth/login
    alt Authentification réussie
        API-->>AS: { user, token }
        AS-->>AS: Stocke token dans localStorage
        AS-->>AS: Met à jour state (user, isAuthenticated)
        AS-->>R: Redirection vers /tasks
    else Authentification échouée
        API-->>AS: 401 Unauthorized
        AS-->>LP: Erreur affichée
        LP-->>U: "Email ou mot de passe incorrect"
    end

Guard de route (ProtectedRoute)

// src/components/common/ProtectedRoute.tsx

/**
 * Composant de protection des routes.
 * Vérifie l'authentification et optionnellement les rôles requis.
 * Redirige vers /login si non authentifié.
 * Affiche un message d'accès refusé si rôle insuffisant.
 */
function ProtectedRoute({
  children,
  requiredRoles,
}: {
  children: React.ReactNode;
  requiredRoles?: Role[];
}) {
  const { isAuthenticated, hasAnyRole } = useAuthStore();

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  if (requiredRoles && !hasAnyRole(requiredRoles)) {
    return (
      <div className="flex items-center justify-center h-full" role="alert">
        <Card className="max-w-md">
          <CardHeader>
            <CardTitle>Accès refusé</CardTitle>
          </CardHeader>
          <CardContent>
            <p>Vous n'avez pas les permissions nécessaires pour accéder à cette page.</p>
            <p className="text-sm text-gray-500 mt-2">
              Rôles requis : {requiredRoles.join(', ')}
            </p>
          </CardContent>
        </Card>
      </div>
    );
  }

  return <>{children}</>;
}

Matrice des permissions UI

La matrice ci-dessous définit ce que chaque rôle peut voir et faire dans l’interface. Elle est le miroir côté frontend de la matrice RBAC du backend (Exigence 5).

Élément UI viewer developer manager admin
Voir la liste des tâches
Voir le détail d’une tâche
Bouton « Créer une tâche »
Modifier une tâche (propre)
Modifier une tâche (autre)
S’auto-attribuer une tâche
Voir la liste des projets
Bouton « Créer un projet »
Accéder à la page Équipes
Accéder au Dashboard
Gérer les utilisateurs

Utilitaire de permissions

// src/lib/permissions.ts

type Permission =
  | 'task:create'
  | 'task:edit-own'
  | 'task:edit-any'
  | 'task:self-assign'
  | 'project:create'
  | 'team:manage'
  | 'dashboard:view'
  | 'user:manage';

/** Matrice rôle → permissions */
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
  viewer: [],
  developer: ['task:create', 'task:edit-own', 'task:self-assign'],
  manager: ['task:create', 'task:edit-own', 'task:edit-any', 'task:self-assign', 'project:create', 'team:manage', 'dashboard:view'],
  admin: ['task:create', 'task:edit-own', 'task:edit-any', 'task:self-assign', 'project:create', 'team:manage', 'dashboard:view', 'user:manage'],
};

/** Vérifie si un utilisateur a une permission donnée */
function hasPermission(user: User, permission: Permission): boolean {
  return user.roles.some((role) => ROLE_PERMISSIONS[role].includes(permission));
}

/** Vérifie si un utilisateur peut éditer une tâche spécifique */
function canEditTask(user: User, task: Task): boolean {
  if (hasPermission(user, 'task:edit-any')) return true;
  if (hasPermission(user, 'task:edit-own') && task.createdBy === user.id) return true;
  return false;
}

10. Responsive Design et Accessibilité

Stratégie Responsive

L’application utilise une approche mobile-first avec Tailwind CSS. Les breakpoints suivent la convention Tailwind :

Breakpoint Largeur min Comportement
sm 640px Sidebar masquée, menu hamburger
md 768px Grille 2 colonnes pour les cartes
lg 1024px Sidebar visible, grille 3 colonnes
xl 1280px Layout étendu avec plus d’espace

Adaptations par composant

Accessibilité (WCAG 2.1 AA)

L’application vise la conformité WCAG 2.1 niveau AA. Shadcn/UI (basé sur Radix UI) fournit des primitives accessibles par défaut.

Principes appliqués

  1. Perceptible
    • Contrastes de couleurs suffisants (ratio 4.5:1 minimum pour le texte)
    • Textes alternatifs pour les icônes (aria-label)
    • Indicateurs visuels non basés uniquement sur la couleur (badges avec texte + couleur)
    • Messages d’erreur associés aux champs via aria-describedby
  2. Utilisable
    • Navigation complète au clavier (Tab, Shift+Tab, Enter, Escape)
    • Focus visible sur tous les éléments interactifs (focus-visible:ring-2)
    • Skip links pour accéder directement au contenu principal
    • Pas de piège au clavier dans les modales (Radix UI gère le focus trap)
  3. Compréhensible
    • Labels explicites sur tous les champs de formulaire (<Label htmlFor>)
    • Messages d’erreur clairs et contextuels
    • Langue de la page définie (<html lang="fr">)
    • Navigation cohérente et prévisible
  4. Robuste
    • HTML sémantique (<main>, <nav>, <header>, <article>)
    • Rôles ARIA appropriés (role="alert", role="status", role="region")
    • aria-live="polite" pour les mises à jour dynamiques (notifications, toasts)
    • Composants Radix UI testés avec les lecteurs d’écran

Attributs ARIA utilisés

Composant Attributs ARIA
TaskCard role="article", aria-label="Tâche: {description}"
TaskStatusBadge aria-label="Statut: {label}"
TaskPriorityBadge aria-label="Priorité: {label}"
Formulaires aria-invalid, aria-describedby, aria-required
Messages d’erreur role="alert"
Indicateur de retard role="status"
Compteurs dashboard role="region", aria-label
Barre de progression aria-label="{rate}% complété"
Sidebar navigation role="navigation", aria-label="Navigation principale"

11. Stratégie de Tests

Approche hybride : TDD Unit + E2E

Conformément aux standards du projet, le frontend utilise une stratégie de tests hybride à deux couches complémentaires :

Couche 1 — Tests unitaires TDD (logique isolée, feedback continu)

Appliquer TDD (test-first) pour toute la logique frontend isolée qui ne dépend pas du navigateur ou du backend :

Règles des tests unitaires TDD : - Les tests s’exécutent sans navigateur, sans backend et sans DOM - Les tests doivent être rapides (millisecondes) et exécutés en continu - Dériver les cas de test directement des critères d’acceptation (scénarios Gherkin) du requirements.md

Structure des tests unitaires :

frontend/tests/
├── unit/
│   ├── lib/               # Tests pour validators, permissions, utilitaires
│   └── stores/            # Tests pour les stores Zustand

Couche 2 — Tests E2E (comportement utilisateur, validation aux checkpoints)

Les tests E2E avec Playwright valident l’intégration complète : composants + routing + communication API + permissions.

Configuration Playwright

// playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  retries: 1,
  workers: process.env.CI ? 1 : undefined,
  reporter: [['html'], ['list']],
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
});

Plan de tests E2E par exigence

Release 1 — MVP

Fichier de test Exigence couverte Scénarios clés
tests/e2e/tasks.spec.ts Exigences 1, 3, 4, 6 Création de tâche, changement de statut, modification de priorité, affichage des échéances
tests/e2e/projects.spec.ts Exigence 2 Création de projet, association de tâches, affichage des tâches d’un projet
tests/e2e/auth.spec.ts Exigence 5 Connexion, déconnexion, restrictions par rôle
tests/e2e/validation.spec.ts Exigence 7 Rejet description vide, rejet statut invalide, avertissement échéance passée

Release 2 — Collaboration

Fichier de test Exigence couverte Scénarios clés
tests/e2e/assignments.spec.ts Exigence 9 Attribution de tâche, attributions multiples, consultation des tâches attribuées, auto-attribution developer
tests/e2e/teams.spec.ts Exigence 10 Création d’équipe, ajout de membres, limite de 6 membres

Release 3 — Pilotage

Fichier de test Exigence couverte Scénarios clés
tests/e2e/dashboard.spec.ts Exigence 11 Compteurs par statut, filtrage par projet, taux de complétion
tests/e2e/history.spec.ts Exigence 12 Affichage de l’historique des modifications
tests/e2e/notifications.spec.ts Exigence 13 Affichage des notifications, marquage comme lue

Exemple de test E2E

// tests/e2e/tasks.spec.ts

import { test, expect } from '@playwright/test';

test.describe('Gestion des tâches', () => {
  test.beforeEach(async ({ page }) => {
    // Connexion en tant que developer
    await page.goto('/login');
    await page.getByLabel('Email').fill('developer@example.com');
    await page.getByLabel('Mot de passe').fill('password123');
    await page.getByRole('button', { name: 'Se connecter' }).click();
    await expect(page).toHaveURL('/tasks');
  });

  test('Exigence 1.1 — Création d\'une tâche complète', async ({ page }) => {
    await page.getByRole('button', { name: 'Créer une tâche' }).click();

    await page.getByLabel('Description').fill('Corriger le bug #42');
    await page.getByLabel('Priorité').selectOption('high');
    await page.getByLabel('Échéance').fill('2025-12-31T18:00');

    await page.getByRole('button', { name: 'Créer la tâche' }).click();

    // Vérifier que la tâche apparaît dans la liste
    await expect(page.getByText('Corriger le bug #42')).toBeVisible();
    await expect(page.getByText('Haute')).toBeVisible();
    await expect(page.getByText('À faire')).toBeVisible();
  });

  test('Exigence 1.4 — Rejet d\'une description vide', async ({ page }) => {
    await page.getByRole('button', { name: 'Créer une tâche' }).click();

    // Soumettre sans description
    await page.getByRole('button', { name: 'Créer la tâche' }).click();

    // Vérifier le message d'erreur
    await expect(page.getByText('La description est obligatoire')).toBeVisible();
  });

  test('Exigence 3.1 — Changement de statut', async ({ page }) => {
    // Cliquer sur une tâche existante
    await page.getByText('Corriger le bug #42').click();

    // Changer le statut
    await page.getByLabel('Statut').selectOption('in-progress');
    await page.getByRole('button', { name: 'Mettre à jour' }).click();

    // Vérifier le nouveau statut
    await expect(page.getByText('En cours')).toBeVisible();
  });

  test('Exigence 5.3 — Viewer ne peut pas créer de tâche', async ({ page }) => {
    // Se déconnecter et se reconnecter en tant que viewer
    await page.getByRole('button', { name: 'Déconnexion' }).click();
    await page.getByLabel('Email').fill('viewer@example.com');
    await page.getByLabel('Mot de passe').fill('password123');
    await page.getByRole('button', { name: 'Se connecter' }).click();

    // Vérifier que le bouton "Créer une tâche" n'est pas visible
    await expect(page.getByRole('button', { name: 'Créer une tâche' })).not.toBeVisible();
  });

  test('Exigence 6.2 — Identification des tâches en retard', async ({ page }) => {
    // Vérifier qu'une tâche en retard affiche l'indicateur
    const overdueTask = page.locator('[data-testid="task-card"]').filter({
      hasText: 'En retard',
    });
    await expect(overdueTask).toBeVisible();
  });
});

12. Types Frontend

Types partagés (miroir du backend)

Les types frontend reprennent les types définis dans backend/src/types/index.ts avec des adaptations pour le contexte client (dates en string ISO au lieu de Date, types d’entrée pour les formulaires).

// src/types/index.ts

/** Types du domaine — miroir des types backend */
export type Status = 'todo' | 'in-progress' | 'completed';
export type Priority = 'low' | 'medium' | 'high';
export type Role = 'admin' | 'manager' | 'developer' | 'viewer';

/** Tâche telle que reçue de l'API (dates en string ISO) */
export interface Task {
  id: string;
  description: string;
  status: Status;
  priority: Priority;
  projectId: string | null;
  dueDate: string | null;       // ISO 8601 string (pas Date)
  isOverdue: boolean;
  createdAt: string;
  createdBy: string;
  updatedAt: string;
  updatedBy: string | null;
  completedAt: string | null;
}

export interface Project {
  id: string;
  name: string;
  description: string | null;
  teamId: string | null;
  createdAt: string;
  createdBy: string;
}

export interface User {
  id: string;
  email: string;
  name: string;
  roles: Role[];
  createdAt: string;
}

export interface Team {
  id: string;
  name: string;
  memberIds: string[];
  createdAt: string;
  createdBy: string;
}

export interface Assignment {
  id: string;
  taskId: string;
  userId: string;
  assignedAt: string;
  assignedBy: string;
}

export interface Notification {
  id: string;
  userId: string;
  taskId: string;
  type: 'due-soon' | 'overdue';
  isRead: boolean;
  createdAt: string;
}

export interface HistoryEntry {
  id: string;
  taskId: string;
  field: 'status' | 'priority';
  previousValue: string;
  newValue: string;
  changedBy: string;
  changedAt: string;
}

/** Types d'erreur API */
export interface ApiError {
  error: string;
  code: string;
  details?: Record<string, unknown>;
}

export interface ApiResponseWithWarnings<T> {
  data: T;
  warnings?: string[];
}

/** Types d'entrée pour les formulaires */
export interface CreateTaskInput {
  description: string;
  priority?: Priority;
  dueDate?: string | null;
  projectId?: string | null;
}

export interface UpdateTaskInput {
  description?: string;
  status?: Status;
  priority?: Priority;
  dueDate?: string | null;
}

export interface CreateProjectInput {
  name: string;
  description?: string | null;
  teamId?: string | null;
}

export interface CreateTeamInput {
  name: string;
}

/** Types UI pour les filtres et le tri */
export interface TaskFilters {
  status?: Status;
  priority?: Priority;
  projectId?: string;
  assignedTo?: string;
  sortBy?: 'priority' | 'dueDate' | 'createdAt' | 'status';
  sortOrder?: 'asc' | 'desc';
  page?: number;
  limit?: number;
}

/** Type pour les statistiques du dashboard */
export interface DashboardStats {
  totalTasks: number;
  statusCounts: Record<Status, number>;
  completionRate: number;
  overdueTasks: number;
}

/** Type pour les réponses paginées de l'API */
export interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

13. Gestion des Erreurs Frontend

Stratégie globale

graph TD
    subgraph "Erreurs Réseau"
        E1["Pas de connexion\n(fetch échoue)"]
        E2["Timeout"]
    end

    subgraph "Erreurs API"
        E3["400 — Validation"]
        E4["401 — Non authentifié"]
        E5["403 — Non autorisé"]
        E6["404 — Introuvable"]
        E7["500 — Erreur serveur"]
    end

    subgraph "Erreurs Client"
        E8["Erreur React\n(composant crash)"]
    end

    E1 -->|"Toast"| T1["'Connexion au serveur impossible.\nVérifiez votre réseau.'"]
    E2 -->|"Toast"| T2["'La requête a expiré.\nRéessayez.'"]
    E3 -->|"Inline"| T3["Messages sous les champs\ndu formulaire"]
    E4 -->|"Redirect"| T4["Redirection vers /login"]
    E5 -->|"Toast"| T5["'Vous n'avez pas les\npermissions nécessaires.'"]
    E6 -->|"Page"| T6["Page 404 ou message\n'Ressource introuvable'"]
    E7 -->|"Toast"| T7["'Une erreur inattendue\nest survenue.'"]
    E8 -->|"ErrorBoundary"| T8["Page d'erreur avec\nbouton 'Réessayer'"]

ErrorBoundary

// src/components/common/ErrorBoundary.tsx

/**
 * Capture les erreurs React non gérées et affiche une page de secours.
 * Empêche le crash complet de l'application.
 */
class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="flex items-center justify-center h-screen" role="alert">
          <Card className="max-w-md">
            <CardHeader>
              <CardTitle>Une erreur est survenue</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-gray-600">
                L'application a rencontré un problème inattendu.
              </p>
              <Button
                className="mt-4"
                onClick={() => {
                  this.setState({ hasError: false, error: null });
                  window.location.href = '/';
                }}
              >
                Retour à l'accueil
              </Button>
            </CardContent>
          </Card>
        </div>
      );
    }
    return this.props.children;
  }
}

Classe d’erreur API personnalisée

// src/api/client.ts

/**
 * Erreur typée pour les réponses API en erreur.
 * Permet de distinguer les types d'erreur dans les handlers.
 */
class ApiClientError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number,
    public details?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'ApiClientError';
  }

  get isValidationError() { return this.statusCode === 400; }
  get isForbidden() { return this.statusCode === 403; }
  get isNotFound() { return this.statusCode === 404; }
  get isServerError() { return this.statusCode === 500; }
}

14. Constantes et Configuration

// src/lib/constants.ts

/** Labels français pour les statuts */
export const STATUS_LABELS: Record<Status, string> = {
  'todo': 'À faire',
  'in-progress': 'En cours',
  'completed': 'Terminée',
};

/** Labels français pour les priorités */
export const PRIORITY_LABELS: Record<Priority, string> = {
  'low': 'Basse',
  'medium': 'Moyenne',
  'high': 'Haute',
};

/** Labels français pour les rôles */
export const ROLE_LABELS: Record<Role, string> = {
  'admin': 'Administrateur',
  'manager': 'Gestionnaire',
  'developer': 'Développeur',
  'viewer': 'Observateur',
};

/** Ordre de tri des priorités (high = 0, low = 2) */
export const PRIORITY_ORDER: Record<Priority, number> = {
  'high': 0,
  'medium': 1,
  'low': 2,
};

/** URL de base de l'API */
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';

15. Traçabilité Exigences ↔︎ Composants

Exigence Composants Frontend Hooks Validation
1 — Création de tâches TaskForm, TaskList useCreateTaskMutation createTaskSchema
2 — Organisation en projets ProjectForm, ProjectCard, ProjectDetailPage useProjectsQuery, useProjectTasksQuery createProjectSchema
3 — Suivi du statut TaskStatusBadge, TaskDetailPage useUpdateTaskMutation updateTaskSchema (status)
4 — Gestion des priorités TaskPriorityBadge, TaskFilters useUpdateTaskMutation updateTaskSchema (priority)
5 — Gestion des rôles ProtectedRoute, permissions.ts useAuthStore ROLE_PERMISSIONS
6 — Suivi des échéances TaskCard (indicateur retard), TaskForm (dueDate) useTasksQuery (sortBy: dueDate) createTaskSchema (dueDate)
7 — Validation des données TaskForm (erreurs inline), validators.ts Mutations (onError) Tous les schémas Zod
8 — API RESTful api/client.ts, api/tasks.ts, api/projects.ts Tous les hooks TanStack Query
9 — Attribution de tâches TaskDetailPage (section attributions) useAssignments
10 — Collaboration en équipe TeamList, TeamCard, TeamForm useTeams createTeamSchema
11 — Tableau de bord StatusCounters, CompletionRate, OverdueTasks useDashboard
12 — Historique TaskDetailPage (onglet historique) useTaskHistory
13 — Notifications Header (badge), NotificationsPage useNotifications