BTS SIO SLAM — Promotion 2025–2026

Projet Salle de Sport
Marc Colvert

Application web intranet de gestion pour une salle de sport — PHP / MySQL / Bootstrap 5

PHP 8 (PDO) MySQL / PhpMyAdmin HTML5 CSS3 Bootstrap 5 Kanban Trello WAMP 64
MA
Mohamed Amine
Dev · Planning · CSS
SF
Sofiane Fodil
Dev · BDD · Dashboard
AX
Axel Santin
Dev · Adhérents · CRUD
RM
Rafael Marta
SISR · Dev · Réservations
NE
Nesrine
SISR · Tests · Déploiement

Réalisations par membre

MA

Mohamed Amine Hidaoui

// Développeur · Planning Public · Planning Intranet · Bannière · CSS · Corrections
Planning Public Planning (intranet) Menu Bannière Hero CSS global Corrections bugs
<?php
/* ============================================================
 * planning_public.php – Planning des séances accessible sans connexion
 * Affiche un calendrier FullCalendar en lecture seule.
 * Les visiteurs peuvent cliquer sur une séance pour voir les détails
 * et sont invités à se connecter pour réserver.
 * ============================================================ */

require_once "config/db.php"; // Connexion PDO (pas d'auth : page publique)

// --- Récupération des séances pour FullCalendar ---
// LEFT JOIN sur coach : une séance peut ne pas avoir de coach assigné
$sql = "
SELECT s.id_seance, a.nom_activite, c.prenom AS coach,
       s.date_heure_debut, s.duree_minutes, s.capacite_max
FROM seance s
JOIN activite a  ON a.id_activite = s.id_activite
LEFT JOIN coach c ON c.id_coach   = s.id_coach
";

// Construction du tableau d'événements au format attendu par FullCalendar
$events = [];
foreach ($pdo->query($sql)->fetchAll() as $r) {
    $events[] = [
        "id"    => $r['id_seance'],
        "title" => $r['nom_activite'],  // Texte affiché sur le bloc calendrier
        "start" => $r['date_heure_debut'],
        "extendedProps" => [            // Données accessibles via JS lors du clic
            "coach"    => $r['coach'] ?? '—',
            "duree"    => $r['duree_minutes'],
            "capacite" => $r['capacite_max']
        ]
    ];
}
?>
<?php
/* ============================================================
 * planning.php – Vue calendrier du planning des séances (intranet)
 * Affiche les séances dans FullCalendar avec possibilité d'ajouter,
 * modifier et supprimer directement depuis le calendrier.
 * ============================================================ */

require_once "includes/auth.php"; // Vérifie que l'utilisateur est connecté
require_once "config/db.php";

// --- Traitement du formulaire (ajout ou modification via la modale) ---
if ($_POST) {
    if (empty($_POST['id_seance'])) {
        // Insertion d'une nouvelle séance (id_coach peut être null)
        $pdo->prepare("INSERT INTO seance
            (id_activite, id_coach, date_heure_debut, duree_minutes, capacite_max)
            VALUES (?,?,?,?,?)")
            ->execute([
                $_POST['id_activite'],
                !empty($_POST['id_coach']) ? $_POST['id_coach'] : null,
                $_POST['date_heure_debut'],
                $_POST['duree_minutes'],
                $_POST['capacite_max']
            ]);
    } else {
        // Mise à jour d'une séance existante
        $pdo->prepare("UPDATE seance
            SET id_activite=?, id_coach=?, date_heure_debut=?,
                duree_minutes=?, capacite_max=?
            WHERE id_seance=?")
            ->execute([
                $_POST['id_activite'],
                !empty($_POST['id_coach']) ? $_POST['id_coach'] : null,
                $_POST['date_heure_debut'],
                $_POST['duree_minutes'],
                $_POST['capacite_max'],
                $_POST['id_seance']
            ]);
    }
    header("Location: planning.php");
    exit;
}

// --- Suppression via ?delete=ID ---
if (isset($_GET['delete'])) {
    $pdo->prepare("DELETE FROM seance WHERE id_seance=?")->execute([$_GET['delete']]);
    header("Location: planning.php");
    exit;
}

// --- Récupération de toutes les séances avec jointures ---
$data = $pdo->query("
    SELECT s.id_seance, a.nom_activite, c.prenom AS coach_prenom, c.nom AS coach_nom,
           s.date_heure_debut, s.duree_minutes, s.capacite_max
    FROM seance s
    JOIN activite a  ON a.id_activite = s.id_activite
    LEFT JOIN coach c ON c.id_coach   = s.id_coach
    ORDER BY s.date_heure_debut DESC
")->fetchAll();
<?php
/* ============================================================
 * hero_banner.php – Bannière animée partagée entre toutes les pages
 * Reçoit $hero_title et $hero_subtitle depuis la page appelante.
 * Utilisée aussi bien sur les pages publiques que l'intranet.
 * ============================================================ */
?>
<div class="hero-banner mb-4">
    <div class="hero-content">
        <!-- Icône Bootstrap Icons + titre dynamique -->
        <h1 class="hero-title">
            <i class="bi bi-trophy"></i>
            <?= htmlspecialchars($hero_title) ?>
        </h1>
        <p class="hero-subtitle"><?= htmlspecialchars($hero_subtitle) ?></p>
    </div>
</div>

Observations

Rôle principal

Développeur — Planning public (accessible sans connexion) et planning intranet avec FullCalendar. Responsable de la bannière hero partagée, du menu, du CSS global et des corrections de bugs d'affichage.

Choix technique notable

Utilisation de FullCalendar 6 pour la vue calendrier : les séances PHP sont sérialisées en JSON directement dans la page et injectées dans le calendrier JS, permettant une vue mensuelle/hebdomadaire sans rechargement.

Bugs corrigés

Correction de l'affichage de la liste des coachs (en binôme avec Sofiane), et correction de la redirection sur la page de login (en binôme avec Axel).

Commentaires & review

Commentaires de l'ensemble du code du projet (maintenabilité) et review des fonctionnalités. Modification des images du site.

SF

Sofiane Fodil

// Développeur · Base de données · Dashboard · Coachs · Séances · Types Abonnement
Base de données Dashboard Coachs Séances Types Abonnement Logs & CSS Correction bugs
<?php
/* ============================================================
 * dashboard.php – Tableau de bord principal de l'intranet
 * Affiche les statistiques globales de la salle Colvert.
 * ============================================================ */

require_once "includes/auth.php"; // Vérifie que l'utilisateur est connecté
require_once "config/db.php";

// --- Compteurs globaux pour les cartes de statistiques ---
$adherents = $pdo->query("SELECT COUNT(*) FROM Adherent")->fetchColumn();
$seances   = $pdo->query("SELECT COUNT(*) FROM Seance")->fetchColumn();
$resas     = $pdo->query("SELECT COUNT(*) FROM Reservation")->fetchColumn();
$abos      = $pdo->query("SELECT COUNT(*) FROM Abonnement WHERE etat='Valide'")->fetchColumn();

include "includes/header.php";
include "includes/menu.php";

$hero_title    = "Tableau de bord";
$hero_subtitle = "Salle de Sport Marc Colvert – Vue d'ensemble";
include "includes/hero_banner.php";
?>

<!-- Cartes de statistiques rapides -->
<div class="row">
    <div class="col-md-3 mb-3">
        <div class="card shadow p-3 text-center">
            <h6>Adhérents</h6>
            <h2><?= $adherents ?></h2>
        </div>
    </div>
    <div class="col-md-3 mb-3">
        <div class="card shadow p-3 text-center">
            <h6>Séances</h6>
            <h2><?= $seances ?></h2>
        </div>
    </div>
    <div class="col-md-3 mb-3">
        <div class="card shadow p-3 text-center">
            <h6>Réservations</h6>
            <h2><?= $resas ?></h2>
        </div>
    </div>
    <div class="col-md-3 mb-3">
        <div class="card shadow p-3 text-center">
            <h6>Abonnements actifs</h6>
            <h2><?= $abos ?></h2>
        </div>
    </div>
</div>

<?php include "includes/footer.php"; ?>
<?php
/* ============================================================
 * coachs.php – Gestion de l'équipe encadrante
 * ============================================================ */

require_once "includes/auth.php";
require_once "config/db.php";

if ($_POST) {
    if (empty($_POST['id_coach'])) {
        $pdo->prepare("INSERT INTO coach (nom, prenom, specialite, telephone) VALUES (?,?,?,?)")
            ->execute([$_POST['nom'], $_POST['prenom'], $_POST['specialite'], $_POST['telephone']]);
    } else {
        $pdo->prepare("UPDATE coach SET nom=?, prenom=?, specialite=?, telephone=? WHERE id_coach=?")
            ->execute([$_POST['nom'], $_POST['prenom'], $_POST['specialite'], $_POST['telephone'], $_POST['id_coach']]);
    }
    header("Location: coachs.php");
    exit;
}

if (isset($_GET['edit'])) {
    $e = $pdo->prepare("SELECT * FROM coach WHERE id_coach=?");
    $e->execute([$_GET['edit']]);
    $edit = $e->fetch();
}

if (isset($_GET['delete'])) {
    $pdo->prepare("DELETE FROM coach WHERE id_coach=?")->execute([$_GET['delete']]);
    header("Location: coachs.php");
    exit;
}

// --- Statistiques ---
$total_coachs  = $pdo->query("SELECT COUNT(*) FROM coach")->fetchColumn();
$total_seances = $pdo->query("SELECT COUNT(*) FROM seance WHERE id_coach IS NOT NULL")->fetchColumn();

// Coach ayant le plus grand nombre de séances assignées
$coach_plus_actif = $pdo->query("
    SELECT c.prenom, c.nom, COUNT(s.id_seance) AS nb
    FROM coach c
    LEFT JOIN seance s ON s.id_coach = c.id_coach
    GROUP BY c.id_coach
    ORDER BY nb DESC
    LIMIT 1
")->fetch();

$data = $pdo->query("SELECT * FROM coach ORDER BY nom")->fetchAll();
<?php
/* ============================================================
 * seance.php – Gestion des séances de sport
 * Chaque séance est liée à une activité et optionnellement à un coach.
 * ============================================================ */

require_once "includes/auth.php";
require_once "config/db.php";

if ($_POST) {
    if (empty($_POST['id_seance'])) {
        $pdo->prepare("INSERT INTO seance
            (id_activite, id_coach, date_heure_debut, duree_minutes, capacite_max)
            VALUES(?,?,?,?,?)")
            ->execute([
                $_POST['id_activite'],
                $_POST['id_coach'] ?: null, // null si aucun coach sélectionné
                $_POST['date_heure_debut'],
                $_POST['duree_minutes'],
                $_POST['capacite_max']
            ]);
    } else {
        $pdo->prepare("UPDATE seance
            SET id_activite=?, id_coach=?, date_heure_debut=?,
                duree_minutes=?, capacite_max=?
            WHERE id_seance=?")
            ->execute([
                $_POST['id_activite'],
                $_POST['id_coach'] ?: null,
                $_POST['date_heure_debut'],
                $_POST['duree_minutes'],
                $_POST['capacite_max'],
                $_POST['id_seance']
            ]);
    }
    header("Location: seance.php");
    exit;
}

// --- Statistiques ---
$total_seances = $pdo->query("SELECT COUNT(*) FROM seance")->fetchColumn();
$capacite_moy  = $pdo->query("SELECT AVG(capacite_max) FROM seance")->fetchColumn();
// Prochaine séance à venir
$prochaine = $pdo->query("
    SELECT date_heure_debut FROM seance
    WHERE date_heure_debut >= NOW()
    ORDER BY date_heure_debut ASC LIMIT 1
")->fetchColumn();

// --- Liste avec jointures activité + coach ---
$data = $pdo->query("
    SELECT s.*, a.nom_activite, c.nom AS coach_nom, c.prenom AS coach_prenom
    FROM seance s
    JOIN activite a  ON s.id_activite = a.id_activite
    LEFT JOIN coach c ON s.id_coach   = c.id_coach
    ORDER BY s.date_heure_debut DESC
")->fetchAll();

Observations

Rôle principal

Conception et mise en place de la base de données MySQL (en binôme avec Axel et Rafael). Développement du dashboard, des pages Coachs, Séances, Types Abonnement, de la page de logs et du CSS global.

Choix technique notable

La page coachs.php calcule dynamiquement le coach le plus actif via une agrégation SQL (COUNT + GROUP BY + ORDER BY + LIMIT 1), ce qui évite tout calcul côté PHP.

Bug corrigé

Inversion du signe de la monnaie dans la page abonnements (montant affiché en négatif) — corrigé directement dans la requête SQL de calcul du revenu total.

Base de données

8 tables : adherent, coach, activite, seance, reservation, abonnement, typeabonnement, utilisateur. Clés étrangères avec contraintes d'intégrité référentielle.

AX

Axel Santin

// Développeur · Adhérents · Activités · Abonnements · Index · CSS
Adhérents Activités Abonnements Index CSS Corrections CRUD
<?php
/* ============================================================
 * adherent.php – Gestion des adhérents
 * ============================================================ */

require_once "includes/auth.php";
require_once "config/db.php";

// --- Traitement du formulaire (ajout ou modification) ---
if ($_POST) {
    if (empty($_POST['id_adherent'])) {
        $pdo->prepare("INSERT INTO adherent
            (nom, prenom, email, mot_de_passe, telephone, date_inscription, statut)
            VALUES (?,?,?,?,?,?,?)")
            ->execute([
                $_POST['nom'], $_POST['prenom'], $_POST['email'],
                $_POST['mot_de_passe'], $_POST['telephone'],
                $_POST['date_inscription'], $_POST['statut']
            ]);
    } else {
        $pdo->prepare("UPDATE adherent
            SET nom=?, prenom=?, email=?, mot_de_passe=?,
                telephone=?, date_inscription=?, statut=?
            WHERE id_adherent=?")
            ->execute([
                $_POST['nom'], $_POST['prenom'], $_POST['email'],
                $_POST['mot_de_passe'], $_POST['telephone'],
                $_POST['date_inscription'], $_POST['statut'],
                $_POST['id_adherent']
            ]);
    }
    header("Location: adherent.php");
    exit;
}

// --- Pré-chargement si modification via ?edit=ID ---
if (isset($_GET['edit'])) {
    $e = $pdo->prepare("SELECT * FROM adherent WHERE id_adherent=?");
    $e->execute([$_GET['edit']]);
    $edit = $e->fetch();
}

// --- Suppression via ?delete=ID ---
if (isset($_GET['delete'])) {
    $pdo->prepare("DELETE FROM adherent WHERE id_adherent=?")->execute([$_GET['delete']]);
    header("Location: adherent.php");
    exit;
}

// --- Statistiques ---
$total   = $pdo->query("SELECT COUNT(*) FROM adherent")->fetchColumn();
$actifs  = $pdo->query("SELECT COUNT(*) FROM adherent WHERE statut='Actif'")->fetchColumn();
$inactif = $pdo->query("SELECT COUNT(*) FROM adherent WHERE statut='Inactif'")->fetchColumn();

$data = $pdo->query("SELECT * FROM adherent ORDER BY nom")->fetchAll();
<?php
/* ============================================================
 * activite.php – Gestion des activités sportives
 * Affiche des statistiques par niveau de difficulté.
 * ============================================================ */

require_once "includes/auth.php";
require_once "config/db.php";

if ($_POST) {
    if (empty($_POST['id_activite'])) {
        $pdo->prepare("INSERT INTO activite(nom_activite, description, difficulte)
            VALUES(?,?,?)")
            ->execute([$_POST['nom_activite'], $_POST['description'], $_POST['difficulte']]);
    } else {
        $pdo->prepare("UPDATE activite
            SET nom_activite=?, description=?, difficulte=?
            WHERE id_activite=?")
            ->execute([
                $_POST['nom_activite'], $_POST['description'],
                $_POST['difficulte'], $_POST['id_activite']
            ]);
    }
    header("Location: activite.php");
    exit;
}

if (isset($_GET['delete'])) {
    $pdo->prepare("DELETE FROM activite WHERE id_activite=?")->execute([$_GET['delete']]);
    header("Location: activite.php");
    exit;
}

// --- Statistiques par niveau de difficulté ---
$total      = $pdo->query("SELECT COUNT(*) FROM activite")->fetchColumn();
$faciles    = $pdo->query("SELECT COUNT(*) FROM activite WHERE difficulte='Facile'")->fetchColumn();
$difficiles = $pdo->query("SELECT COUNT(*) FROM activite WHERE difficulte='Difficile'")->fetchColumn();

$data = $pdo->query("SELECT * FROM activite ORDER BY nom_activite")->fetchAll();
<?php
/* ============================================================
 * abonnements.php – Gestion des abonnements des adhérents
 * Le prix est automatiquement rempli depuis le type sélectionné.
 * ============================================================ */

require_once "includes/auth.php";
require_once "config/db.php";

if ($_POST) {
    if (empty($_POST['id_abonnement'])) {
        $pdo->prepare("INSERT INTO abonnement
            (id_adherent, id_type_abo, date_debut, date_fin, prix_paye, etat)
            VALUES (?,?,?,?,?,?)")
            ->execute([
                $_POST['id_adherent'], $_POST['id_type_abo'],
                $_POST['date_debut'],  $_POST['date_fin'],
                $_POST['prix_paye'],   $_POST['etat']
            ]);
    } else {
        $pdo->prepare("UPDATE abonnement
            SET id_adherent=?, id_type_abo=?, date_debut=?,
                date_fin=?, prix_paye=?, etat=?
            WHERE id_abonnement=?")
            ->execute([
                $_POST['id_adherent'], $_POST['id_type_abo'],
                $_POST['date_debut'],  $_POST['date_fin'],
                $_POST['prix_paye'],   $_POST['etat'],
                $_POST['id_abonnement']
            ]);
    }
    header("Location: abonnements.php");
    exit;
}

// --- Statistiques financières ---
$total_abos   = $pdo->query("SELECT COUNT(*) FROM abonnement")->fetchColumn();
$abos_valides = $pdo->query("SELECT COUNT(*) FROM abonnement WHERE etat='Valide'")->fetchColumn();
$revenu_total = $pdo->query("SELECT SUM(prix_paye) FROM abonnement WHERE etat='Valide'")->fetchColumn();

// --- Liste avec jointures adhérent + type ---
$data = $pdo->query("
    SELECT a.id_abonnement, ad.nom, ad.prenom, t.libelle,
           a.date_debut, a.date_fin, a.prix_paye, a.etat
    FROM abonnement a
    JOIN adherent      ad ON ad.id_adherent = a.id_adherent
    JOIN typeabonnement t  ON t.id_type_abo  = a.id_type_abo
    ORDER BY a.date_fin DESC
")->fetchAll();

Observations

Rôle principal

Développement des pages Adhérents, Activités, Abonnements et de la page d'accueil (index). Correction des bugs d'ajout, suppression et modification sur les pages Adhérents et Activités.

Choix technique notable

La page abonnements.php remplit automatiquement le champ prix via JS à partir de l'attribut data-prix présent sur chaque option du sélecteur de type d'abonnement.

Bugs corrigés

Corrections CRUD sur adherent.php et activite.php : problèmes de redirection après POST, et gestion de la valeur nulle sur le champ email non obligatoire.

Base de données

Co-responsable avec Sofiane et Rafael de la conception du schéma de base de données. Définition des tables et contraintes relationnelles.

RM

Rafael Marta

// SISR & Développeur · Réservations · Index (avec Axel) · CSS
Réservations Index (co-dev) CSS Base de données Correction affichage tableau
<?php
/* ============================================================
 * reservation.php – Gestion des réservations de séances
 * Permet aux adhérents de réserver une séance.
 * Vérifie la capacité disponible avant insertion.
 * ============================================================ */

require_once "includes/auth.php";
require_once "config/db.php";

if ($_POST) {
    if (empty($_POST['id_reservation'])) {
        $pdo->prepare("INSERT INTO reservation
            (id_adherent, id_seance, date_reservation, statut_reservation)
            VALUES (?,?,?,?)")
            ->execute([
                $_POST['id_adherent'],
                $_POST['id_seance'],
                $_POST['date_reservation'],
                $_POST['statut_reservation']
            ]);
    } else {
        $pdo->prepare("UPDATE reservation
            SET id_adherent=?, id_seance=?,
                date_reservation=?, statut_reservation=?
            WHERE id_reservation=?")
            ->execute([
                $_POST['id_adherent'],
                $_POST['id_seance'],
                $_POST['date_reservation'],
                $_POST['statut_reservation'],
                $_POST['id_reservation']
            ]);
    }
    header("Location: reservation.php");
    exit;
}

if (isset($_GET['delete'])) {
    $pdo->prepare("DELETE FROM reservation WHERE id_reservation=?")->execute([$_GET['delete']]);
    header("Location: reservation.php");
    exit;
}

// --- Statistiques ---
$total_resas     = $pdo->query("SELECT COUNT(*) FROM reservation")->fetchColumn();
$resas_confirmees = $pdo->query(
    "SELECT COUNT(*) FROM reservation WHERE statut_reservation='Confirmée'"
)->fetchColumn();

// --- Listes déroulantes pour le formulaire ---
$adherents = $pdo->query("SELECT id_adherent, nom, prenom FROM adherent ORDER BY nom")->fetchAll();
$seances   = $pdo->query("
    SELECT s.id_seance, a.nom_activite, s.date_heure_debut
    FROM seance s
    JOIN activite a ON a.id_activite = s.id_activite
    ORDER BY s.date_heure_debut DESC
")->fetchAll();

// --- Liste complète des réservations avec jointures ---
$data = $pdo->query("
    SELECT r.id_reservation, ad.nom, ad.prenom,
           a.nom_activite, s.date_heure_debut,
           r.date_reservation, r.statut_reservation
    FROM reservation r
    JOIN adherent ad ON ad.id_adherent = r.id_adherent
    JOIN seance   s  ON s.id_seance   = r.id_seance
    JOIN activite a  ON a.id_activite  = s.id_activite
    ORDER BY r.date_reservation DESC
")->fetchAll();

Observations

Rôle principal

Profil SISR + Développeur. Développement de la page Réservations et co-développement de la page Index avec Axel. Co-concepteur de la base de données avec Sofiane et Axel.

Jointures SQL

La liste des réservations utilise une triple jointure (reservation → adherent, seance, activite) pour afficher un tableau complet sans aucun traitement PHP supplémentaire.

Bug corrigé

Correction de l'affichage du tableau des réservations — les colonnes Activité et Date n'apparaissaient pas correctement à cause d'alias SQL incorrects sur les colonnes jointes.

Statuts gérés

Les réservations distinguent deux états : Confirmée et Annulée, cohérents avec le type ENUM défini dans la base de données.

NE

Nesrine

// SISR & Développeuse · Tests · Visualisation finale · Déploiement
Tests du code final Visualisation rendu Déploiement WAMP

Nesrine intervient en qualité de profil SISR avec participation au développement. Son rôle s'est concentré sur la phase de finalisation et de déploiement du projet :

  • Visualisation et validation du rendu final de l'application — vérification du comportement de chaque page (navigation, formulaires, affichages).
  • Participation active au déploiement du dossier projet dans C:\wamp64\www sur le serveur local partagé en LAN.
  • Tests fonctionnels sur l'ensemble des pages CRUD : adhérents, coachs, séances, abonnements, réservations, activités.
  • Vérification de la cohérence des redirections après les opérations POST et de l'affichage des messages de confirmation.

Observations

Rôle

Profil SISR orienté tests et déploiement. Garantit la conformité du rendu livré en s'assurant que l'application est opérationnelle dans l'environnement cible WAMP 64.

Valeur ajoutée

La phase de test réalisée par un membre non développeur de la fonctionnalité permet de détecter des bugs d'affichage ou de comportement que les développeurs responsables n'auraient pas repérés.

Bilan du projet

✅ Points forts

  • Base de données relationnelle complète couvrant tous les besoins métier (8 tables)
  • Pages CRUD fonctionnelles sur toutes les entités du projet
  • Séparation claire entre partie publique (planning_public) et intranet sécurisé par authentification
  • Intégration de FullCalendar 6 pour la visualisation du planning en vue mensuelle et hebdomadaire
  • Organisation Kanban efficace via Trello (4 colonnes : À faire / En cours / Terminé / Ressources)
  • Collaboration en temps réel via LAN partagé — modifications visibles instantanément par tous
  • Déploiement local opérationnel sous WAMP 64 — application fonctionnelle et testée
  • Code commenté sur l'ensemble du projet (maintenabilité assurée par Mohamed Amine)

⚡ Axes d'amélioration

  • Hachage des mots de passe côté base de données (actuellement stockés en clair)
  • Contrôle de la capacité maximale lors de la création d'une réservation (pas encore implémenté côté serveur)
  • Sécurisation des suppressions avec tokens CSRF pour prévenir les suppressions accidentelles via URL directe
  • Gestion des rôles différenciés (Admin vs Coach) dans la table utilisateur
  • Pagination des tableaux sur les grandes listes (adhérents, réservations)
  • Tests unitaires et fonctionnels à ajouter pour fiabiliser les futures évolutions