Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

Implémenter la Gestion des Rôles Firebase avec Flutter

Un cours complet pour maîtriser Implémenter la Gestion des Rôles Firebase avec Flutter.

0 chapitres 0 QCM Accès gratuit
 
  1. Objectif

    • À l'issue de ce tutoriel, vous serez capable d'intégrer un système complet de gestion des rôles dans une application Flutter en utilisant Firebase Authentication et Cloud Firestore, avec VS Code comme environnement de développement.
    • Vous apprendrez à :
      • Créer et configurer un projet Firebase avec Auth et Firestore.
      • Structurer votre projet Flutter selon l'architecture MVC (Modèle - Vue - Contrôleur).
      • Définir un enum UserRole pour représenter les 4 rôles.
      • Créer une interface de connexion unique redirigeant chaque utilisateur vers son dashboard.
      • Permettre à l'admin de créer tous les comptes avec email, mot de passe et rôle.
      • Sécuriser Firestore avec des Security Rules basées sur les rôles.
      • Gérer les erreurs de connexion et naviguer entre les écrans.
  2. Présentation

    • La gestion des rôles (Role-Based Access Control — RBAC) est un concept fondamental en développement d'applications multi-utilisateurs. Plutôt que de donner les mêmes droits à tous les utilisateurs, on définit des rôles, et chaque rôle dispose de permissions spécifiques.
    • Dans ce tutoriel, nous utilisons deux services Firebase :
      • Firebase Authentication: gère l'identité (email + mot de passe) de chaque utilisateur.
      • Cloud Firestore: stocke le profil de l'utilisateur, notamment son rôle.
    • ? Concept clé : Le rôle N'EST PAS stocké dans Firebase Auth. Il est stocké dans Firestore (collection users), et lu après chaque connexion pour décider vers quel écran rediriger l'utilisateur.
  3. Les 4 rôles de l'application

    • L'application distingue 4 types d'utilisateurs, chacun avec ses propres permissions :
      • admin? : Crée et supprime tous les comptes utilisateurs, voit tous les utilisateurs.
      • enseignant? : Crée et gère ses propres cours (propriétaire des cours).
      • etudiant? : Consulte les cours disponibles et peut s'inscrire.
      • responsable_etudiant? : Supervise la liste des étudiants et tous les cours.
    • ⚠️ Important : Tous les utilisateurs se connectent via la MÊME interface (LoginView). C'est après la connexion que l'application lit le rôle dans Firestore et redirige vers le bon dashboard.
  4. Architecture du projet (MVC)

    • Le projet est organisé selon le pattern MVC (Modèle - Vue - Contrôleur) avec la structure suivante :
        lib/
        ├── main.dart                          ← Point d'entrée de l'application
        ├── firebase_options.dart              ← Configuration Firebase (généré automatiquement)
        │
        ├── models/                            ← ? MODÈLES (données)
        │   ├── user_model.dart                ← Modèle utilisateur + enum UserRole
        │   └── cours_model.dart               ← Modèle cours
        │
        ├── controllers/                       ← ? CONTRÔLEURS (logique métier)
        │   ├── auth_controller.dart           ← Connexion / Déconnexion / Lecture rôle
        │   └── user_controller.dart           ← CRUD utilisateurs et cours
        │
        └── views/                             ← ?️  VUES (interface utilisateur)
            ├── login_view.dart                ← Interface de connexion unique
            ├── admin/
            │   └── admin_dashboard.dart       ← ? Dashboard Administrateur
            ├── enseignant/
            │   └── enseignant_dashboard.dart  ← ? Dashboard Enseignant
            ├── etudiant/
            │   └── etudiant_dashboard.dart    ← ? Dashboard Étudiant
            └── responsable/
                └── responsable_dashboard.dart ← ? Dashboard Responsable Étudiant
  5. Configuration du projet

    1. Étape 1 : Créer un projet Firebase
      • Va sur Firebase Console
      • Clique sur "Ajouter un projet"
      • Donne un nom à ton projet, accepte les conditions, clique sur Continuer
      • Active Firebase Authentication → méthode Email/Mot de passe
      • Active Cloud Firestore en mode production
    2. Étape 2 : Ajouter Firebase à ton projet Flutter
      • Créer une nouvelle application Flutter
        • flutter create gestion_roles
        • Ouvrez le projet dans un IDE (Android Studio, VS Code…)
      • Se connecter à Firebase et configurer FlutterFire CLI
        • Si ce n'est pas encore fait, installe FlutterFire CLI :dart pub global activate flutterfire_cli
        • Puis connecte-toi :flutterfire login
        • Et configure Firebase pour ce projet :flutterfire configure
        • Lors de la configuration :
          • Choisis ton projet Firebase existant.
          • Sélectionne Android comme plateforme.
          • Entre le bon nom de package (ex: com.example.gestionroles).
          • Cela génère un fichier firebase_options.dart dans lib/.
    3. Configurer Firebase dans le projet Flutter
      • a. Copier le google-services.json:
        • Téléchargé depuis la console Firebase (section Android).
        • Colle-le dans : android/app/google-services.json
      • b. Modifier android/build.gradle:
          buildscript {
              dependencies {
                  classpath("com.google.gms:google-services:4.3.15")
              }
              repositories {
                  google()
                  mavenCentral()
              }
          }
      • c. Modifier android/app/build.gradle:
          plugins {
              id("com.android.application")
              id("kotlin-android")
              id("dev.flutter.flutter-gradle-plugin")
              id("com.google.gms.google-services")
          }
      • d. Modifier le fichier : android/app/build.gradle.kts
      • Dans ton fichier android/app/build.gradle.kts, ajoute (ou modifie) ce bloc dans android :
          android {
              ndkVersion = "27.0.12077973"
              // ... le reste de ta configuration Android
          }
      • e. Ajouter les dépendances dans pubspec.yaml:
          dependencies:
            flutter:
              sdk: flutter
            firebase_core:   ^2.24.2
            firebase_auth:   ^4.16.0
            cloud_firestore: ^4.14.0
  6. Créer le compte Administrateur

    • Avant de lancer l'application, vous devez créer manuellement le compte administrateur. Contrairement aux autres utilisateurs qui seront créés depuis l'application, le premier compte admin doit être configuré directement dans la console Firebase en deux étapes obligatoires: une dans Firebase Authentication et une dans Firestore.
    • ⚠️ Pourquoi deux étapes ? Firebase Auth vérifie uniquement l'identité (email + mot de passe). Le rôle de l'utilisateur est stocké dans Firestore. Sans le document Firestore, l'application ne peut pas connaître le rôle et retournera null après la connexion.
      1. Étape 1 : Créer le compte dans Firebase Authentication
        • Allez sur console.firebase.google.com
        • Votre projet → Authentication → Users → Add user
        • Saisissez l'email et le mot de passe de l'admin
        • Cliquez Add user
        • ⚠️ Copiez l'UID généré (ex : abc123xyz...) — vous en aurez besoin à l'étape suivante
      2. Étape 2 : Créer le document dans Firestore ⚠️ OBLIGATOIRE
        • Sans cette étape, l'admin peut se connecter mais l'application ne trouvera pas son rôle et retournera null.
        • Votre projet → Firestore → collection users → Add document
        • Dans Document ID, collez l'UID exact copié à l'étape 1
        • Ajoutez les champs suivants :
            {
              "email":     "admin@univ.tn",   // type : string
              "nom":       "Admin",           // type : string
              "prenom":    "Super",           // type : string
              "role":      "admin",           // type : string  ← valeur exacte obligatoire
              "createdAt": [date actuelle]    // type : timestamp
            }
        • ⚠️ La valeur du champ role doit être exactement "admin" (en minuscule), c'est la valeur lue par UserRoleExtension.fromString() dans le modèle.
    • Pourquoi ces deux étapes sont-elles nécessaires ?
        Firebase Auth  →  vérifie email + mot de passe ✅
        Firestore      →  stocke le rôle "admin"        ✅
                                  ↓
           AuthController.signIn() lit le champ "role" dans Firestore
                                  ↓
                 _redirectByRole() → AdminDashboard ?
    • Une fois ce compte admin créé, il pourra se connecter via l'application et créer tous les autres comptes (enseignants, étudiants, responsables) directement depuis son dashboard sans passer par la console Firebase.
  7. Fichiers réalisés

    1. Fichier firebase_options.dart
          
          // File: lib/firebase_options.dart
          import 'package:firebase_core/firebase_core.dart';
          import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
          
          /*
          ? INSTRUCTIONS POUR LES ÉTUDIANTS:
          
          1. Récupérez vos clés Firebase sur https://console.firebase.google.com/
          2. Remplacez les valeurs ci-dessous par VOS propres clés
          3. NE COMMITTEZ PAS ce fichier avec vos vraies clés sur Git!
          4. Ajoutez-le dans .gitignore pour la sécurité
          
          ? Où trouver les valeurs:
          
          • apiKey → google-services.json → client → api_key → current_key
          • appId → google-services.json → client → client_info → mobilesdk_app_id  
          • messagingSenderId → google-services.json → project_info → project_number
          • projectId → google-services.json → project_info → project_id
          • storageBucket → google-services.json → project_info → storage_bucket
          */
          
          /// CONFIGURATION FIREBASE POUR TOUTES LES PLATEFORMES
          class DefaultFirebaseOptions {
            
            /// CONFIGURATION ANDROID
            static const FirebaseOptions android = FirebaseOptions(
              apiKey: 'AIzaSyDMA991uRiFusn1DyxoZgzF0_HOf8Ab6fs',
              appId: '1:210885415501:android:4758b89c6d137dc2018204',
              messagingSenderId: '210885415501',
              projectId: 'projet-livre-firestore',
              storageBucket: 'projet-livre-firestore.firebasestorage.app',
            );
          
            /// CONFIGURATION iOS (À décommenter si nécessaire)
            /*
            static const FirebaseOptions ios = FirebaseOptions(
              apiKey: 'AIzaSyA1B2C3d4E5f6G7H8I9J0K1L2M3N4O5P6Q',
              appId: '1:835574812736:ios:abc123def456ghi789',
              messagingSenderId: '835574812736',
              projectId: 'gestion-bibliotheque-7ed2f',
              storageBucket: 'gestion-bibliotheque-7ed2f.firebasestorage.app',
              iosBundleId: 'com.example.gestionBibliotheque',
            );
            */
          
            /// CONFIGURATION WEB (À décommenter si nécessaire)
            /*
            static const FirebaseOptions web = FirebaseOptions(
              apiKey: '...',
              appId: '...',
              messagingSenderId: '...',
              projectId: '...',
              authDomain: '...',
              storageBucket: '...',
            );
            */
          
            /// ? DÉTECTION AUTOMATIQUE DE LA PLATEFORME
            static FirebaseOptions get currentPlatform {
              // Si on est sur le web
              if (kIsWeb) {
                // return web; // Décommenter quand web est configuré
                throw UnsupportedError(
                  'Configuration Web non encore configurée. '
                  'Veuillez décommenter et configurer la section web ci-dessus.',
                );
              }
              
              // Si on est sur mobile
              switch (defaultTargetPlatform) {
                case TargetPlatform.android:
                  return android;
                case TargetPlatform.iOS:
                  // return ios; // Décommenter quand iOS est configuré
                  throw UnsupportedError(
                    'Configuration iOS non encore configurée. '
                    'Veuillez décommenter et configurer la section iOS ci-dessus.',
                  );
                default:
                  throw UnsupportedError(
                    'Plateforme non supportée: ${defaultTargetPlatform}. '
                    'Seul Android est configuré pour le moment.',
                  );
              }
            }
          }
          
          
    2. Fichier models/user_model.dart
      • Ce fichier définit l'enum UserRole et le modèle UserModel avec les méthodes de conversion vers/depuis Firestore :
          // File: lib/models/user_model.dart
           
          /// ? ENUM DES RÔLES DISPONIBLES
          enum UserRole {
            admin,
            enseignant,
            etudiant,
            responsable_etudiant,
          }
           
          /// Extension pour convertir le rôle en/depuis String (Firestore)
          extension UserRoleExtension on UserRole {
            String get name {
              switch (this) {
                case UserRole.admin:                return 'admin';
                case UserRole.enseignant:           return 'enseignant';
                case UserRole.etudiant:             return 'etudiant';
                case UserRole.responsable_etudiant: return 'responsable_etudiant';
              }
            }
           
            String get displayName {
              switch (this) {
                case UserRole.admin:                return 'Administrateur';
                case UserRole.enseignant:           return 'Enseignant';
                case UserRole.etudiant:             return 'Étudiant';
                case UserRole.responsable_etudiant: return 'Responsable Étudiant';
              }
            }
           
            static UserRole fromString(String value) {
              switch (value) {
                case 'admin':                return UserRole.admin;
                case 'enseignant':           return UserRole.enseignant;
                case 'etudiant':             return UserRole.etudiant;
                case 'responsable_etudiant': return UserRole.responsable_etudiant;
                default: throw Exception('Rôle inconnu: $value');
              }
            }
          }
           
          /// ? MODÈLE UTILISATEUR
          class UserModel {
            final String   uid;
            final String   email;
            final String   nom;
            final String   prenom;
            final UserRole role;
            final DateTime createdAt;
           
            UserModel({
              required this.uid,
              required this.email,
              required this.nom,
              required this.prenom,
              required this.role,
              required this.createdAt,
            });
           
            /// Depuis Firestore → UserModel
            factory UserModel.fromMap(String uid, Map<String, dynamic> map) {
              return UserModel(
                uid:       uid,
                email:     map['email']  ?? '',
                nom:       map['nom']    ?? '',
                prenom:    map['prenom'] ?? '',
                role:      UserRoleExtension.fromString(map['role'] ?? 'etudiant'),
                createdAt: DateTime.now(),
              );
            }
           
            /// UserModel → Firestore
            Map<String, dynamic> toMap() {
              return {
                'email':     email,
                'nom':       nom,
                'prenom':    prenom,
                'role':      role.name,
                'createdAt': createdAt,
              };
            }
           
            String get fullName => '$prenom $nom';
          }
    3. Fichier controllers/auth_controller.dart
      • Le contrôleur d'authentification gère la connexion, la déconnexion et la lecture du rôle depuis Firestore:
          // File: lib/controllers/auth_controller.dart
          import 'package:firebase_auth/firebase_auth.dart';
          import 'package:cloud_firestore/cloud_firestore.dart';
          import '../models/user_model.dart';
           
          /// ? CONTRÔLEUR D'AUTHENTIFICATION
          class AuthController {
            final FirebaseAuth      _auth      = FirebaseAuth.instance;
            final FirebaseFirestore _firestore = FirebaseFirestore.instance;
           
            /// ? CONNEXION avec email + mot de passe
            Future<UserModel?> signIn(String email, String password) async {
              try {
                final credential = await _auth.signInWithEmailAndPassword(
                  email:    email.trim(),
                  password: password,
                );
                if (credential.user == null) return null;
                // Récupérer le profil + rôle depuis Firestore
                return await getUserById(credential.user!.uid);
              } on FirebaseAuthException catch (e) {
                throw _mapAuthError(e);
              }
            }
           
            /// ? Lire le profil utilisateur depuis Firestore
            Future<UserModel?> getUserById(String uid) async {
              final doc = await _firestore.collection('users').doc(uid).get();
              if (!doc.exists) return null;
              return UserModel.fromMap(doc.id, doc.data()!);
            }
           
            /// ? DÉCONNEXION
            Future<void> signOut() async => await _auth.signOut();
           
            /// ?️ Convertir les erreurs Firebase en messages lisibles
            String _mapAuthError(FirebaseAuthException e) {
              switch (e.code) {
                case 'invalid-credential': return 'Email ou mot de passe incorrect.';
                case 'user-not-found':     return 'Aucun compte trouvé avec cet email.';
                case 'wrong-password':     return 'Mot de passe incorrect.';
                case 'user-disabled':      return 'Ce compte a été désactivé.';
                case 'too-many-requests':  return 'Trop de tentatives. Réessayez plus tard.';
                default: return 'Erreur: ${e.message}';
              }
            }
          }
    4. Fichier controllers/user_controller.dart
      • Ce contrôleur gère la création des comptes (réservée à l'admin) et toutes les opérations sur les cours :
          // File: lib/controllers/user_controller.dart
          import 'package:firebase_auth/firebase_auth.dart';
          import 'package:cloud_firestore/cloud_firestore.dart';
          import '../models/user_model.dart';
           
          /// ? CONTRÔLEUR UTILISATEURS
          class UserController {
            final FirebaseAuth      _auth = FirebaseAuth.instance;
            final FirebaseFirestore _db   = FirebaseFirestore.instance;
           
            // ── ADMIN : CRÉER UN COMPTE ──────────────────────────────────
            Future<UserModel> createUser({
              required String   email,
              required String   password,
              required String   nom,
              required String   prenom,
              required UserRole role,
            }) async {
              // 1. Créer dans Firebase Auth
              final credential = await _auth.createUserWithEmailAndPassword(
                email: email.trim(), password: password,
              );
              final uid = credential.user!.uid;
           
              // 2. Sauvegarder le profil + rôle dans Firestore
              final user = UserModel(
                uid: uid, email: email.trim(),
                nom: nom.trim(), prenom: prenom.trim(),
                role: role, createdAt: DateTime.now(),
              );
              await _db.collection('users').doc(uid).set(user.toMap());
              return user;
            }
           
            // ── LIRE TOUS LES UTILISATEURS (admin) ──────────────────────
            Future<List<UserModel>> getAllUsers() async {
              final snap = await _db.collection('users')
                  .orderBy('createdAt', descending: true).get();
              return snap.docs
                  .map((d) => UserModel.fromMap(d.id, d.data()))
                  .toList();
            }
           
            // ── LIRE PAR RÔLE (responsable) ─────────────────────────────
            Future<List<UserModel>> getUsersByRole(UserRole role) async {
              final snap = await _db.collection('users')
                  .where('role', isEqualTo: role.name).get();
              return snap.docs
                  .map((d) => UserModel.fromMap(d.id, d.data()))
                  .toList();
            }
           
            // ── SUPPRIMER UN UTILISATEUR (admin) ────────────────────────
            Future<void> deleteUserFromFirestore(String uid) async {
              await _db.collection('users').doc(uid).delete();
            }
           
            // ── CRÉER UN COURS (enseignant) ──────────────────────────────
            Future<void> createCours({
              required String enseignantId,
              required String enseignantNom,
              required String titre,
              required String description,
              required String matiere,
            }) async {
              await _db.collection('cours').add({
                'titre':           titre,
                'description':     description,
                'matiere':         matiere,
                'enseignantId':    enseignantId,
                'enseignantNom':   enseignantNom,
                'nombreEtudiants': 0,
                'createdAt':       FieldValue.serverTimestamp(),
              });
            }
           
            // ── COURS D'UN ENSEIGNANT ────────────────────────────────────
            Future<List<Map<String, dynamic>>> getCoursParEnseignant(String id) async {
              final snap = await _db.collection('cours')
                  .where('enseignantId', isEqualTo: id).get();
              return snap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
            }
           
            // ── TOUS LES COURS ───────────────────────────────────────────
            Future<List<Map<String, dynamic>>> getTousCours() async {
              final snap = await _db.collection('cours')
                  .orderBy('createdAt', descending: true).get();
              return snap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
            }
           
            Future<void> deleteCours(String id) async {
              await _db.collection('cours').doc(id).delete();
            }
          }
    5. Fichier main.dart
        // File: lib/main.dart
        import 'package:flutter/material.dart';
        import 'package:firebase_core/firebase_core.dart';
        import 'firebase_options.dart';
        import 'views/login_view.dart';
        
        void main() async {
          WidgetsFlutterBinding.ensureInitialized();
        
          // ? Initialisation Firebase
          await Firebase.initializeApp(
            options: DefaultFirebaseOptions.currentPlatform,
          );
        
          runApp(const MyApp());
        }
        
        class MyApp extends StatelessWidget {
          const MyApp({super.key});
        
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              title: 'Gestion des Rôles Firebase',
              debugShowCheckedModeBanner: false,
              theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A237E)),
                useMaterial3: true,
                fontFamily: 'Roboto',
              ),
              home: const LoginView(),
            );
          }
        }
    6. Fichier views/login_view.dart
      • Interface de connexion unique pour tous les rôles. Après authentification, l'utilisateur est redirigé vers son dashboard selon son rôle :
          // File: lib/views/login_view.dart
          import 'package:flutter/material.dart';
          import '../controllers/auth_controller.dart';
          import '../models/user_model.dart';
          import 'admin/admin_dashboard.dart';
          import 'enseignant/enseignant_dashboard.dart';
          import 'etudiant/etudiant_dashboard.dart';
          import 'responsable/responsable_dashboard.dart';
           
          class LoginView extends StatefulWidget {
            const LoginView({super.key});
            @override
            State<LoginView> createState() => _LoginViewState();
          }
           
          class _LoginViewState extends State<LoginView> {
            final _formKey            = GlobalKey<FormState>();
            final _emailController    = TextEditingController();
            final _passwordController = TextEditingController();
            final _authController     = AuthController();
            bool    _isLoading   = false;
            bool    _obscurePass = true;
            String? _errorMessage;
           
            Future<void> _login() async {
              if (!_formKey.currentState!.validate()) return;
              setState(() { _isLoading = true; _errorMessage = null; });
              try {
                final user = await _authController.signIn(
                  _emailController.text, _passwordController.text);
                if (user == null) {
                  setState(() => _errorMessage = 'Utilisateur introuvable dans la base.');
                  return;
                }
                if (!mounted) return;
                _redirectByRole(user);
              } catch (e) {
                setState(() => _errorMessage = e.toString());
              } finally {
                if (mounted) setState(() => _isLoading = false);
              }
            }
           
            /// ? Redirection vers le bon dashboard selon le rôle
            void _redirectByRole(UserModel user) {
              Widget destination;
              switch (user.role) {
                case UserRole.admin:
                  destination = AdminDashboard(currentUser: user);       break;
                case UserRole.enseignant:
                  destination = EnseignantDashboard(currentUser: user);  break;
                case UserRole.etudiant:
                  destination = EtudiantDashboard(currentUser: user);    break;
                case UserRole.responsable_etudiant:
                  destination = ResponsableDashboard(currentUser: user); break;
              }
              Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (_) => destination),
              );
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: Container(
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                      colors: [Color(0xFF1A237E), Color(0xFF283593), Color(0xFF3949AB)],
                    ),
                  ),
                  child: SafeArea(
                    child: Center(
                      child: SingleChildScrollView(
                        padding: const EdgeInsets.all(24),
                        child: Form(
                          key: _formKey,
                          child: Column(
                            children: [
                              const Icon(Icons.school_rounded, size: 80, color: Colors.white),
                              const SizedBox(height: 16),
                              const Text('Gestion des Rôles',
                                style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold,
                                                 color: Colors.white)),
                              const SizedBox(height: 32),
                              TextFormField(
                                controller: _emailController,
                                decoration: const InputDecoration(
                                  labelText: 'Adresse email',
                                  prefixIcon: Icon(Icons.email_outlined),
                                  filled: true, fillColor: Colors.white,
                                ),
                                validator: (v) => v!.isEmpty ? 'Email requis' : null,
                              ),
                              const SizedBox(height: 16),
                              TextFormField(
                                controller: _passwordController,
                                obscureText: _obscurePass,
                                decoration: InputDecoration(
                                  labelText: 'Mot de passe',
                                  prefixIcon: const Icon(Icons.lock_outline),
                                  filled: true, fillColor: Colors.white,
                                  suffixIcon: IconButton(
                                    icon: Icon(_obscurePass
                                      ? Icons.visibility_off_outlined
                                      : Icons.visibility_outlined),
                                    onPressed: () =>
                                      setState(() => _obscurePass = !_obscurePass),
                                  ),
                                ),
                                validator: (v) =>
                                  v!.length < 6 ? 'Minimum 6 caractères' : null,
                              ),
                              if (_errorMessage != null) ...[
                                const SizedBox(height: 12),
                                Text(_errorMessage!,
                                  style: const TextStyle(color: Colors.redAccent)),
                              ],
                              const SizedBox(height: 24),
                              ElevatedButton(
                                onPressed: _isLoading ? null : _login,
                                child: _isLoading
                                  ? const CircularProgressIndicator(color: Colors.white)
                                  : const Text('Se connecter'),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              );
            }
          }
    7. Fichier views/admin/admin_dashboard.dart
      • Le dashboard admin comporte deux onglets: création de comptes et liste des utilisateurs :
          // File: lib/views/admin/admin_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class AdminDashboard extends StatefulWidget {
            final UserModel currentUser;
            const AdminDashboard({super.key, required this.currentUser});
            @override
            State<AdminDashboard> createState() => _AdminDashboardState();
          }
           
          class _AdminDashboardState extends State<AdminDashboard>
              with SingleTickerProviderStateMixin {
            final _userController = UserController();
            final _authController = AuthController();
            late TabController _tabController;
            List<UserModel> _users = [];
           
            // Champs du formulaire de création
            final _formKey      = GlobalKey<FormState>();
            final _emailCtrl    = TextEditingController();
            final _passwordCtrl = TextEditingController();
            final _nomCtrl      = TextEditingController();
            final _prenomCtrl   = TextEditingController();
            UserRole _selectedRole = UserRole.etudiant;
            bool    _creating   = false;
            String? _createError;
            String? _createSuccess;
           
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadUsers();
            }
           
            Future<void> _loadUsers() async {
              final users = await _userController.getAllUsers();
              setState(() => _users = users);
            }
           
            /// ? L'admin crée un compte avec email, mot de passe et rôle
            Future<void> _createUser() async {
              if (!_formKey.currentState!.validate()) return;
              setState(() { _creating = true; _createError = null; });
              try {
                await _userController.createUser(
                  email:    _emailCtrl.text,
                  password: _passwordCtrl.text,
                  nom:      _nomCtrl.text,
                  prenom:   _prenomCtrl.text,
                  role:     _selectedRole,
                );
                setState(() =>
                  _createSuccess = '✅ Compte créé : ${_prenomCtrl.text} (${_selectedRole.name})');
                _loadUsers();
              } catch (e) {
                setState(() => _createError = e.toString());
              } finally {
                setState(() => _creating = false);
              }
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.red.shade700,
                  title: Text('Admin — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    tabs: const [
                      Tab(icon: Icon(Icons.person_add), text: 'Créer un compte'),
                      Tab(icon: Icon(Icons.people),     text: 'Utilisateurs'),
                    ],
                  ),
                ),
                body: TabBarView(
                  controller: _tabController,
                  children: [
                    _buildCreateUserTab(),
                    _buildUsersListTab(),
                  ],
                ),
              );
            }
           
            Widget _buildCreateUserTab() {
              return SingleChildScrollView(
                padding: const EdgeInsets.all(20),
                child: Form(
                  key: _formKey,
                  child: Column(
                    children: [
                      TextFormField(controller: _prenomCtrl,
                        decoration: const InputDecoration(labelText: 'Prénom'),
                        validator: (v) => v!.isEmpty ? 'Requis' : null),
                      TextFormField(controller: _nomCtrl,
                        decoration: const InputDecoration(labelText: 'Nom'),
                        validator: (v) => v!.isEmpty ? 'Requis' : null),
                      TextFormField(controller: _emailCtrl,
                        decoration: const InputDecoration(labelText: 'Email'),
                        validator: (v) => v!.isEmpty ? 'Requis' : null),
                      TextFormField(controller: _passwordCtrl,
                        obscureText: true,
                        decoration: const InputDecoration(labelText: 'Mot de passe'),
                        validator: (v) => v!.length < 6 ? 'Min. 6 caractères' : null),
                      const SizedBox(height: 16),
                      // Sélection du rôle via des chips colorées
                      Wrap(
                        spacing: 8,
                        children: UserRole.values.map((role) =>
                          ChoiceChip(
                            label: Text(role.name),
                            selected: _selectedRole == role,
                            onSelected: (_) => setState(() => _selectedRole = role),
                          ),
                        ).toList(),
                      ),
                      if (_createError   != null) Text(_createError!,
                        style: const TextStyle(color: Colors.red)),
                      if (_createSuccess != null) Text(_createSuccess!,
                        style: const TextStyle(color: Colors.green)),
                      const SizedBox(height: 16),
                      ElevatedButton.icon(
                        icon: const Icon(Icons.person_add),
                        label: Text(_creating ? 'Création...' : 'Créer le compte'),
                        onPressed: _creating ? null : _createUser,
                      ),
                    ],
                  ),
                ),
              );
            }
           
            Widget _buildUsersListTab() {
              return ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _users.length,
                itemBuilder: (_, i) {
                  final u = _users[i];
                  return ListTile(
                    leading: const Icon(Icons.account_circle),
                    title:    Text(u.fullName),
                    subtitle: Text('${u.email} — ${u.role.name}'),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete_outline, color: Colors.red),
                      onPressed: () async {
                        await _userController.deleteUserFromFirestore(u.uid);
                        _loadUsers();
                      },
                    ),
                  );
                },
              );
            }
          }
    8. Fichier views/enseignant/enseignant_dashboard.dart
      • L'enseignant est propriétaire de ses cours. Ses cours sont filtrés par son UID dans Firestore :
          // File: lib/views/enseignant/enseignant_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class EnseignantDashboard extends StatefulWidget {
            final UserModel currentUser;
            const EnseignantDashboard({super.key, required this.currentUser});
            @override
            State<EnseignantDashboard> createState() => _EnseignantDashboardState();
          }
           
          class _EnseignantDashboardState extends State<EnseignantDashboard>
              with SingleTickerProviderStateMixin {
            final _userController = UserController();
            final _authController = AuthController();
            late TabController _tabController;
            List<Map<String, dynamic>> _cours = [];
           
            final _formKey   = GlobalKey<FormState>();
            final _titreCtrl = TextEditingController();
            final _descCtrl  = TextEditingController();
            final _matCtrl   = TextEditingController();
           
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadCours();
            }
           
            /// Chargement des cours de cet enseignant uniquement
            Future<void> _loadCours() async {
              final cours = await _userController
                  .getCoursParEnseignant(widget.currentUser.uid);
              setState(() => _cours = cours);
            }
           
            /// Création d'un cours (enseignant = propriétaire)
            Future<void> _createCours() async {
              if (!_formKey.currentState!.validate()) return;
              await _userController.createCours(
                titre:        _titreCtrl.text,
                description:  _descCtrl.text,
                matiere:      _matCtrl.text,
                enseignantId:  widget.currentUser.uid,
                enseignantNom: widget.currentUser.fullName,
              );
              _loadCours();
              _tabController.animateTo(1);
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.green.shade700,
                  title: Text('Enseignant — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    tabs: const [
                      Tab(icon: Icon(Icons.add_circle_outline), text: 'Nouveau cours'),
                      Tab(icon: Icon(Icons.menu_book),          text: 'Mes cours'),
                    ],
                  ),
                ),
                body: TabBarView(
                  controller: _tabController,
                  children: [
                    SingleChildScrollView(
                      padding: const EdgeInsets.all(20),
                      child: Form(
                        key: _formKey,
                        child: Column(children: [
                          TextFormField(controller: _titreCtrl,
                            decoration: const InputDecoration(labelText: 'Titre du cours'),
                            validator: (v) => v!.isEmpty ? 'Requis' : null),
                          TextFormField(controller: _matCtrl,
                            decoration: const InputDecoration(labelText: 'Matière'),
                            validator: (v) => v!.isEmpty ? 'Requis' : null),
                          TextFormField(controller: _descCtrl, maxLines: 3,
                            decoration: const InputDecoration(labelText: 'Description'),
                            validator: (v) => v!.isEmpty ? 'Requis' : null),
                          const SizedBox(height: 16),
                          ElevatedButton.icon(
                            icon: const Icon(Icons.save),
                            label: const Text('Publier le cours'),
                            onPressed: _createCours,
                          ),
                        ]),
                      ),
                    ),
                    ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _cours.length,
                      itemBuilder: (_, i) {
                        final c = _cours[i];
                        return ListTile(
                          leading: const Icon(Icons.menu_book, color: Colors.green),
                          title:    Text(c['titre'] ?? ''),
                          subtitle: Text(c['matiere'] ?? ''),
                          trailing: IconButton(
                            icon: const Icon(Icons.delete_outline, color: Colors.red),
                            onPressed: () async {
                              await _userController.deleteCours(c['id']);
                              _loadCours();
                            },
                          ),
                        );
                      },
                    ),
                  ],
                ),
              );
            }
          }
    9. Fichier views/etudiant/etudiant_dashboard.dart
      • L'étudiant a un accès en lecture seule sur les cours. Il peut rechercher et s'inscrire :
          // File: lib/views/etudiant/etudiant_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class EtudiantDashboard extends StatefulWidget {
            final UserModel currentUser;
            const EtudiantDashboard({super.key, required this.currentUser});
            @override
            State<EtudiantDashboard> createState() => _EtudiantDashboardState();
          }
           
          class _EtudiantDashboardState extends State<EtudiantDashboard> {
            final _userController = UserController();
            final _authController = AuthController();
            List<Map<String, dynamic>> _cours = [];
            String _searchQuery = '';
           
            @override
            void initState() {
              super.initState();
              _loadCours();
            }
           
            Future<void> _loadCours() async {
              final cours = await _userController.getTousCours();
              setState(() => _cours = cours);
            }
           
            /// Filtrage des cours par recherche (titre, matière, enseignant)
            List<Map<String, dynamic>> get _filteredCours {
              if (_searchQuery.isEmpty) return _cours;
              return _cours.where((c) {
                final q = _searchQuery.toLowerCase();
                return (c['titre']         ?? '').toLowerCase().contains(q)
                    || (c['matiere']       ?? '').toLowerCase().contains(q)
                    || (c['enseignantNom'] ?? '').toLowerCase().contains(q);
              }).toList();
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.blue.shade700,
                  title: Text('Étudiant — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                ),
                body: Column(children: [
                  Padding(
                    padding: const EdgeInsets.all(12),
                    child: TextField(
                      onChanged: (v) => setState(() => _searchQuery = v),
                      decoration: const InputDecoration(
                        hintText: 'Rechercher un cours...',
                        prefixIcon: Icon(Icons.search),
                        border: OutlineInputBorder(),
                      ),
                    ),
                  ),
                  Expanded(
                    child: ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _filteredCours.length,
                      itemBuilder: (_, i) {
                        final c = _filteredCours[i];
                        return Card(
                          child: ListTile(
                            leading: const Icon(Icons.menu_book, color: Colors.blue),
                            title:    Text(c['titre']   ?? ''),
                            subtitle: Text('${c['matiere'] ?? ''} — Prof. ${c['enseignantNom'] ?? ''}'),
                            trailing: OutlinedButton(
                              onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
                                SnackBar(content: Text('Inscrit à "${c['titre']}"!')),
                              ),
                              child: const Text("S'inscrire"),
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                ]),
              );
            }
          }
    10. Fichier views/responsable/responsable_dashboard.dart
      • Le responsable étudiant a une vue de supervision en lecture seule sur les étudiants et les cours :
          // File: lib/views/responsable/responsable_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class ResponsableDashboard extends StatefulWidget {
            final UserModel currentUser;
            const ResponsableDashboard({super.key, required this.currentUser});
            @override
            State<ResponsableDashboard> createState() => _ResponsableDashboardState();
          }
           
          class _ResponsableDashboardState extends State<ResponsableDashboard>
              with SingleTickerProviderStateMixin {
            final _userController = UserController();
            final _authController = AuthController();
            late TabController _tabController;
            List<UserModel>              _etudiants = [];
            List<Map<String, dynamic>>  _cours     = [];
           
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadEtudiants();
              _loadCours();
            }
           
            Future<void> _loadEtudiants() async {
              final users = await _userController.getUsersByRole(UserRole.etudiant);
              setState(() => _etudiants = users);
            }
           
            Future<void> _loadCours() async {
              final cours = await _userController.getTousCours();
              setState(() => _cours = cours);
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.orange.shade700,
                  title: Text('Responsable — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    tabs: const [
                      Tab(icon: Icon(Icons.people),    text: 'Étudiants'),
                      Tab(icon: Icon(Icons.menu_book), text: 'Tous les cours'),
                    ],
                  ),
                ),
                body: TabBarView(
                  controller: _tabController,
                  children: [
                    // Onglet 1 : liste des étudiants
                    ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _etudiants.length,
                      itemBuilder: (_, i) {
                        final e = _etudiants[i];
                        return ListTile(
                          leading: CircleAvatar(child: Text(e.prenom[0])),
                          title:    Text(e.fullName),
                          subtitle: Text(e.email),
                        );
                      },
                    ),
                    // Onglet 2 : tous les cours (lecture seule)
                    ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _cours.length,
                      itemBuilder: (_, i) {
                        final c = _cours[i];
                        return ListTile(
                          leading: const Icon(Icons.menu_book, color: Colors.orange),
                          title:    Text(c['titre']   ?? ''),
                          subtitle: Text('${c['matiere'] ?? ''} — Prof. ${c['enseignantNom'] ?? ''}'),
                        );
                      },
                    ),
                  ],
                ),
              );
            }
          }
    11. Fichier firestore.rules — Security Rules
      • Copiez ces règles dans Firebase Console → Firestore → Règles pour sécuriser l'accès côté serveur:
          rules_version = '2';
          service cloud.firestore {
            match /databases/{database}/documents {
           
              // Helper : récupérer le rôle de l'utilisateur connecté
              function getUserRole() {
                return get(/databases/$(database)/documents/
                       users/$(request.auth.uid)).data.role;
              }
              function isAdmin()      { return request.auth != null && getUserRole() == 'admin'; }
              function isEnseignant() { return request.auth != null && getUserRole() == 'enseignant'; }
              function isAuth()       { return request.auth != null; }
           
              // ── Collection users ──────────────────────────────────────
              match /users/{userId} {
                allow read:   if isAuth() && (isAdmin() || request.auth.uid == userId);
                allow write:  if isAdmin();
                allow create: if isAdmin();
                allow delete: if isAdmin();
              }
           
              // ── Collection cours ──────────────────────────────────────
              match /cours/{coursId} {
                allow read:   if isAuth();
                allow create: if isEnseignant()
                              && request.resource.data.enseignantId == request.auth.uid;
                allow update, delete:
                              if isAdmin()
                              || (isEnseignant()
                                 && resource.data.enseignantId == request.auth.uid);
              }
            }
          }
  8. Extension de l'application : Gestion de la scolarité

    • Dans cette partie, nous allons étendre l'application en ajoutant la gestion complète de la scolarité : matières, groupes d'étudiants, affectations enseignant/matière et saisie des notes avec affichage du bulletin.
    • Voici les nouvelles collections Firestore ajoutées :
        matieres/
          {matiereId}/
            nom:           "Mathématiques"
            coefficient:   3
            createdBy:     "uid_responsable"
         
        groupes/
          {groupeId}/
            nom:           "G1"
            niveau:        "L2"
            etudiants:     ["uid1", "uid2", ...]   ← liste des UIDs des étudiants
         
        affectations/
          {affectationId}/
            enseignantId:  "uid_enseignant"
            enseignantNom: "Jean Dupont"
            matiereId:     "id_matiere"
            matiereNom:    "Mathématiques"
            groupeId:      "id_groupe"
            groupeNom:     "G1 — L2"
         
        notes/
          {noteId}/
            etudiantId:    "uid_etudiant"
            etudiantNom:   "Ali Ben Salem"
            matiereId:     "id_matiere"
            matiereNom:    "Mathématiques"
            coefficient:   3
            enseignantId:  "uid_enseignant"
            groupeId:      "id_groupe"
            note:          15.5
            semestre:      "S1"
            createdAt:     Timestamp
    • La nouvelle architecture complète du projet est la suivante :
        lib/
        ├── models/
        │   ├── user_model.dart                ← inchangé
        │   ├── matiere_model.dart             ← ? Modèle matière
        │   ├── groupe_model.dart              ← ? Modèle groupe
        │   ├── affectation_model.dart         ← ? Modèle affectation
        │   └── note_model.dart                ← ? Modèle note
        │
        ├── controllers/
        │   ├── auth_controller.dart           ← inchangé
        │   ├── user_controller.dart           ← inchangé
        │   └── scolarite_controller.dart      ← ? Matières, groupes, affectations, notes
        │
        └── views/
            ├── login_view.dart                ← inchangé
            ├── admin/
            │   └── admin_dashboard.dart       ← ? mis à jour (+ onglet affectations)
            ├── enseignant/
            │   └── enseignant_dashboard.dart  ← ? mis à jour (affectations + saisie notes)
            ├── etudiant/
            │   └── etudiant_dashboard.dart    ← ? mis à jour (profil + bulletin)
            └── responsable/
                └── responsable_dashboard.dart ← ? mis à jour (matières, groupes, affectations)
  9. Nouveaux modèles

    1. Fichier models/matiere_model.dart
      • Ce modèle représente une matière avec son coefficient, créée par le responsable étudiant :
          // File: lib/models/matiere_model.dart
           
          class MatiereModel {
            final String id;
            final String nom;
            final double coefficient;
            final String createdBy; // uid du responsable
           
            MatiereModel({
              required this.id,
              required this.nom,
              required this.coefficient,
              required this.createdBy,
            });
           
            factory MatiereModel.fromMap(String id, Map<String, dynamic> map) {
              return MatiereModel(
                id:          id,
                nom:         map['nom'] ?? '',
                coefficient: (map['coefficient'] ?? 1).toDouble(),
                createdBy:   map['createdBy'] ?? '',
              );
            }
           
            Map<String, dynamic> toMap() => {
              'nom':         nom,
              'coefficient': coefficient,
              'createdBy':   createdBy,
            };
          }
    2. Fichier models/groupe_model.dart
      • Un groupe contient un nom, un niveau (ex: L2, M1) et la liste des UIDs des étudiants qui en font partie :
          // File: lib/models/groupe_model.dart
           
          class GroupeModel {
            final String id;
            final String nom;
            final String niveau;
            final List<String> etudiants; // liste des UIDs
           
            GroupeModel({
              required this.id,
              required this.nom,
              required this.niveau,
              required this.etudiants,
            });
           
            String get displayName => '$nom — $niveau';
           
            factory GroupeModel.fromMap(String id, Map<String, dynamic> map) {
              return GroupeModel(
                id:        id,
                nom:       map['nom'] ?? '',
                niveau:    map['niveau'] ?? '',
                etudiants: List<String>.from(map['etudiants'] ?? []),
              );
            }
           
            Map<String, dynamic> toMap() => {
              'nom':       nom,
              'niveau':    niveau,
              'etudiants': etudiants,
            };
          }
    3. Fichier models/affectation_model.dart
      • Une affectation lie un enseignant à une matière et un groupe :
          // File: lib/models/affectation_model.dart
           
          class AffectationModel {
            final String id;
            final String enseignantId;
            final String enseignantNom;
            final String matiereId;
            final String matiereNom;
            final String groupeId;
            final String groupeNom;
            final DateTime createdAt;
           
            AffectationModel({
              required this.id,       required this.enseignantId,
              required this.enseignantNom, required this.matiereId,
              required this.matiereNom,    required this.groupeId,
              required this.groupeNom,     required this.createdAt,
            });
           
            factory AffectationModel.fromMap(String id, Map<String, dynamic> map) {
              return AffectationModel(
                id:            id,
                enseignantId:  map['enseignantId']  ?? '',
                enseignantNom: map['enseignantNom'] ?? '',
                matiereId:     map['matiereId']     ?? '',
                matiereNom:    map['matiereNom']    ?? '',
                groupeId:      map['groupeId']      ?? '',
                groupeNom:     map['groupeNom']     ?? '',
                createdAt:     DateTime.now(),
              );
            }
           
            Map<String, dynamic> toMap() => {
              'enseignantId':  enseignantId, 'enseignantNom': enseignantNom,
              'matiereId':     matiereId,    'matiereNom':    matiereNom,
              'groupeId':      groupeId,     'groupeNom':     groupeNom,
              'createdAt':     createdAt,
            };
          }
    4. Fichier models/note_model.dart
      • Une note est liée à un étudiant, une matière, un groupe et un semestre. Le coefficient est stocké directement pour faciliter le calcul de la moyenne pondérée :
          // File: lib/models/note_model.dart
           
          class NoteModel {
            final String id;
            final String etudiantId;
            final String etudiantNom;
            final String matiereId;
            final String matiereNom;
            final double coefficient;
            final String enseignantId;
            final String groupeId;
            final double note;
            final String semestre;   // "S1" ou "S2"
            final DateTime createdAt;
           
            NoteModel({
              required this.id,           required this.etudiantId,
              required this.etudiantNom,  required this.matiereId,
              required this.matiereNom,   required this.coefficient,
              required this.enseignantId, required this.groupeId,
              required this.note,         required this.semestre,
              required this.createdAt,
            });
           
            factory NoteModel.fromMap(String id, Map<String, dynamic> map) {
              return NoteModel(
                id:           id,
                etudiantId:   map['etudiantId']   ?? '',
                etudiantNom:  map['etudiantNom']  ?? '',
                matiereId:    map['matiereId']    ?? '',
                matiereNom:   map['matiereNom']   ?? '',
                coefficient:  (map['coefficient'] ?? 1).toDouble(),
                enseignantId: map['enseignantId'] ?? '',
                groupeId:     map['groupeId']     ?? '',
                note:         (map['note']        ?? 0).toDouble(),
                semestre:     map['semestre']     ?? 'S1',
                createdAt:    DateTime.now(),
              );
            }
           
            Map<String, dynamic> toMap() => {
              'etudiantId':  etudiantId,  'etudiantNom': etudiantNom,
              'matiereId':   matiereId,   'matiereNom':  matiereNom,
              'coefficient': coefficient, 'enseignantId':enseignantId,
              'groupeId':    groupeId,    'note':        note,
              'semestre':    semestre,    'createdAt':   createdAt,
            };
          }
  10. Nouveau contrôleur : scolarite_controller.dart

    • Ce contrôleur centralise toute la logique métier liée à la scolarité : création et lecture des matières, groupes, affectations et notes, ainsi que le calcul de la moyenne pondérée:
        // File: lib/controllers/scolarite_controller.dart
        import 'package:cloud_firestore/cloud_firestore.dart';
        import '../models/matiere_model.dart';
        import '../models/groupe_model.dart';
        import '../models/affectation_model.dart';
        import '../models/note_model.dart';
        import '../models/user_model.dart';
         
        class ScolariteController {
          final FirebaseFirestore _db = FirebaseFirestore.instance;
         
          // ── MATIÈRES (créées par le responsable) ─────────────────────
          Future<void> createMatiere({
            required String nom, required double coefficient, required String createdBy,
          }) async {
            await _db.collection('matieres').add({
              'nom': nom, 'coefficient': coefficient,
              'createdBy': createdBy, 'createdAt': FieldValue.serverTimestamp(),
            });
          }
         
          Future<List<MatiereModel>> getMatieres() async {
            final snap = await _db.collection('matieres').orderBy('nom').get();
            return snap.docs.map((d) => MatiereModel.fromMap(d.id, d.data())).toList();
          }
         
          Future<void> deleteMatiere(String id) async =>
              await _db.collection('matieres').doc(id).delete();
         
          // ── GROUPES (créés par le responsable) ───────────────────────
          Future<void> createGroupe({
            required String nom, required String niveau,
          }) async {
            await _db.collection('groupes').add({
              'nom': nom, 'niveau': niveau, 'etudiants': [],
              'createdAt': FieldValue.serverTimestamp(),
            });
          }
         
          Future<List<GroupeModel>> getGroupes() async {
            final snap = await _db.collection('groupes').orderBy('niveau').get();
            return snap.docs.map((d) => GroupeModel.fromMap(d.id, d.data())).toList();
          }
         
          /// Ajouter un étudiant à un groupe
          Future<void> ajouterEtudiantGroupe({
            required String groupeId, required String etudiantId,
          }) async {
            await _db.collection('groupes').doc(groupeId).update({
              'etudiants': FieldValue.arrayUnion([etudiantId]),
            });
          }
         
          /// Trouver le groupe d'un étudiant
          Future<GroupeModel?> getGroupeEtudiant(String etudiantId) async {
            final snap = await _db.collection('groupes')
                .where('etudiants', arrayContains: etudiantId).limit(1).get();
            if (snap.docs.isEmpty) return null;
            return GroupeModel.fromMap(snap.docs.first.id, snap.docs.first.data());
          }
         
          /// Récupérer les étudiants d'un groupe
          Future<List<UserModel>> getEtudiantsGroupe(GroupeModel groupe) async {
            if (groupe.etudiants.isEmpty) return [];
            final snap = await _db.collection('users')
                .where(FieldPath.documentId, whereIn: groupe.etudiants).get();
            return snap.docs.map((d) => UserModel.fromMap(d.id, d.data())).toList();
          }
         
          // ── AFFECTATIONS (admin ou responsable) ──────────────────────
          Future<void> createAffectation({
            required String enseignantId, required String enseignantNom,
            required String matiereId,    required String matiereNom,
            required String groupeId,     required String groupeNom,
          }) async {
            await _db.collection('affectations').add({
              'enseignantId': enseignantId, 'enseignantNom': enseignantNom,
              'matiereId':    matiereId,    'matiereNom':    matiereNom,
              'groupeId':     groupeId,     'groupeNom':     groupeNom,
              'createdAt':    FieldValue.serverTimestamp(),
            });
          }
         
          Future<List<AffectationModel>> getAffectations() async {
            final snap = await _db.collection('affectations').get();
            return snap.docs
                .map((d) => AffectationModel.fromMap(d.id, d.data())).toList();
          }
         
          Future<List<AffectationModel>> getAffectationsEnseignant(String uid) async {
            final snap = await _db.collection('affectations')
                .where('enseignantId', isEqualTo: uid).get();
            return snap.docs
                .map((d) => AffectationModel.fromMap(d.id, d.data())).toList();
          }
         
          Future<void> deleteAffectation(String id) async =>
              await _db.collection('affectations').doc(id).delete();
         
          // ── NOTES (saisies par les enseignants) ──────────────────────
          Future<void> saisirNote({
            required String etudiantId,  required String etudiantNom,
            required String matiereId,   required String matiereNom,
            required double coefficient, required String enseignantId,
            required String groupeId,    required double note,
            required String semestre,
          }) async {
            // Vérifier si une note existe déjà pour cet étudiant/matière/semestre
            final existing = await _db.collection('notes')
                .where('etudiantId', isEqualTo: etudiantId)
                .where('matiereId',  isEqualTo: matiereId)
                .where('semestre',   isEqualTo: semestre)
                .limit(1).get();
         
            if (existing.docs.isNotEmpty) {
              // Mettre à jour la note existante
              await _db.collection('notes').doc(existing.docs.first.id)
                  .update({'note': note, 'updatedAt': FieldValue.serverTimestamp()});
            } else {
              // Créer une nouvelle note
              await _db.collection('notes').add({
                'etudiantId':  etudiantId,  'etudiantNom':  etudiantNom,
                'matiereId':   matiereId,   'matiereNom':   matiereNom,
                'coefficient': coefficient, 'enseignantId': enseignantId,
                'groupeId':    groupeId,    'note':         note,
                'semestre':    semestre,    'createdAt':    FieldValue.serverTimestamp(),
              });
            }
          }
         
          Future<List<NoteModel>> getNotesEtudiant(String etudiantId) async {
            final snap = await _db.collection('notes')
                .where('etudiantId', isEqualTo: etudiantId).get();
            return snap.docs.map((d) => NoteModel.fromMap(d.id, d.data())).toList();
          }
         
          Future<List<NoteModel>> getNotesGroupe({
            required String groupeId, required String matiereId, required String semestre,
          }) async {
            final snap = await _db.collection('notes')
                .where('groupeId',  isEqualTo: groupeId)
                .where('matiereId', isEqualTo: matiereId)
                .where('semestre',  isEqualTo: semestre).get();
            return snap.docs.map((d) => NoteModel.fromMap(d.id, d.data())).toList();
          }
         
          /// Calcul de la moyenne pondérée pour un semestre
          double calculerMoyenne(List<NoteModel> notes, String semestre) {
            final notesSemestre = notes.where((n) => n.semestre == semestre).toList();
            if (notesSemestre.isEmpty) return 0;
            double totalPoints = 0, totalCoeff = 0;
            for (final n in notesSemestre) {
              totalPoints += n.note * n.coefficient;
              totalCoeff  += n.coefficient;
            }
            return totalCoeff == 0 ? 0 : totalPoints / totalCoeff;
          }
        }
  11. Dashboards mis à jour

    1. Fichier views/responsable/responsable_dashboard.dart
      • Le dashboard responsable est enrichi de 4 onglets: matières, groupes, affectations et étudiants :
        • Onglet Matières: créer une matière (nom + coefficient) → appel à createMatiere()
        • Onglet Groupes: créer un groupe (nom + niveau), affecter des étudiants via un dialog de sélection
        • Onglet Affectations: lier un enseignant à une matière et un groupe via 3 dropdowns
        • Onglet Étudiants: voir tous les étudiants avec leur groupe affiché
          // File: lib/views/responsable/responsable_dashboard.dart (extrait)
           
          // Chargement parallèle de toutes les données
          Future<void> _loadAll() async {
            final results = await Future.wait([
              _scolariteCtrl.getMatieres(),
              _scolariteCtrl.getGroupes(),
              _scolariteCtrl.getAffectations(),
              _userCtrl.getUsersByRole(UserRole.etudiant),
              _userCtrl.getUsersByRole(UserRole.enseignant),
            ]);
            setState(() {
              _matieres     = results[0] as List<MatiereModel>;
              _groupes      = results[1] as List<GroupeModel>;
              _affectations = results[2] as List<AffectationModel>;
              _etudiants    = results[3] as List<UserModel>;
              _enseignants  = results[4] as List<UserModel>;
            });
          }
           
          // Création d'une matière via dialog
          Future<void> _dialogMatiere() async {
            // dialog avec champs nom + coefficient
            await _scolariteCtrl.createMatiere(
              nom: nomCtrl.text.trim(),
              coefficient: double.tryParse(coeffCtrl.text) ?? 1,
              createdBy: widget.currentUser.uid,
            );
          }
           
          // Ajout d'un étudiant dans un groupe
          Future<void> _ajouterEtudiant(GroupeModel groupe, UserModel etudiant) async {
            await _scolariteCtrl.ajouterEtudiantGroupe(
              groupeId: groupe.id, etudiantId: etudiant.uid,
            );
          }
    2. Fichier views/enseignant/enseignant_dashboard.dart
      • Le dashboard enseignant contient 2 onglets: ses affectations et la saisie des notes :
        • Onglet Mes affectations: liste des matières/groupes auxquels il est affecté (filtrés par son UID)
        • Onglet Saisir notes: sélection affectation + semestre → liste des étudiants du groupe avec champ de note → bouton enregistrer tout
          // File: lib/views/enseignant/enseignant_dashboard.dart (extrait)
           
          // File: lib/views/enseignant/enseignant_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/scolarite_controller.dart';
          import '../../models/user_model.dart';
          import '../../models/affectation_model.dart';
          import '../../models/note_model.dart';
          import '../login_view.dart';
          /// ? DASHBOARD ENSEIGNANT
          /// Fonctionnalités: Voir ses affectations, saisir les notes par groupe/matière
          class EnseignantDashboard extends StatefulWidget {
            final UserModel currentUser;
            const EnseignantDashboard({super.key, required this.currentUser});
          
            @override
            State createState() => _EnseignantDashboardState();
          }
          
          class _EnseignantDashboardState extends State
              with SingleTickerProviderStateMixin {
            final _scolariteCtrl = ScolariteController();
            final _authController      = AuthController();
            late TabController _tabController;
          
            List _affectations = [];
            bool _loading = true;
          
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadAffectations();
            }
          
            @override
            void dispose() {
              _tabController.dispose();
              super.dispose();
            }
          
            Future _loadAffectations() async {
              setState(() => _loading = true);
              try {
                final aff = await _scolariteCtrl
                    .getAffectationsEnseignant(widget.currentUser.uid);
                setState(() => _affectations = aff);
              } finally {
                setState(() => _loading = false);
              }
            }
          
            void _logout() async {
            await _authController.signOut();
            if (!mounted) return;
            Navigator.of(context).pushReplacement(
              MaterialPageRoute(builder: (_) => const LoginView()),
            );
          }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                backgroundColor: const Color(0xFFF3F4F6),
                appBar: AppBar(
                  backgroundColor: Colors.green.shade700,
                  foregroundColor: Colors.white,
                  title: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Espace Enseignant',
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                      Text('Prof. ${widget.currentUser.fullName}',
                          style: const TextStyle(fontSize: 12, color: Colors.white70)),
                    ],
                  ),
                  leading: Container(
                    margin: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.white.withOpacity(0.2),
                      shape: BoxShape.circle,
                    ),
                    child: const Icon(Icons.cast_for_education, color: Colors.white),
                  ),
                  actions: [
                    IconButton(icon: const Icon(Icons.logout_rounded), onPressed: _logout),
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    unselectedLabelColor: Colors.white60,
                    tabs: const [
                      Tab(icon: Icon(Icons.link), text: 'Mes affectations'),
                      Tab(icon: Icon(Icons.edit_note), text: 'Saisir notes'),
                    ],
                  ),
                ),
                body: _loading
                    ? const Center(child: CircularProgressIndicator())
                    : TabBarView(
                        controller: _tabController,
                        children: [
                          _buildAffectations(),
                          _buildSaisieNotes(),
                        ],
                      ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 1 : MES AFFECTATIONS
            // ════════════════════════════════════════════════
          
            Widget _buildAffectations() {
              if (_affectations.isEmpty) {
                return Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.link_off, size: 64, color: Colors.grey.shade300),
                      const SizedBox(height: 12),
                      const Text('Aucune affectation pour le moment',
                          style: TextStyle(color: Colors.grey, fontSize: 16)),
                      const SizedBox(height: 8),
                      const Text('Le responsable doit vous affecter à une matière',
                          style: TextStyle(color: Colors.grey, fontSize: 13)),
                    ],
                  ),
                );
              }
          
              return RefreshIndicator(
                onRefresh: _loadAffectations,
                child: ListView.builder(
                  padding: const EdgeInsets.all(16),
                  itemCount: _affectations.length,
                  itemBuilder: (_, i) {
                    final a = _affectations[i];
                    return Container(
                      margin: const EdgeInsets.only(bottom: 12),
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(14),
                        boxShadow: [BoxShadow(
                            color: Colors.black.withOpacity(0.05),
                            blurRadius: 8, offset: const Offset(0, 3))],
                      ),
                      child: Row(
                        children: [
                          Container(
                            width: 50, height: 50,
                            decoration: BoxDecoration(
                              color: Colors.green.shade100,
                              borderRadius: BorderRadius.circular(12),
                            ),
                            child: Icon(Icons.book, color: Colors.green.shade700, size: 26),
                          ),
                          const SizedBox(width: 14),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(a.matiereNom,
                                    style: const TextStyle(
                                        fontWeight: FontWeight.bold, fontSize: 15)),
                                const SizedBox(height: 4),
                                Row(
                                  children: [
                                    Icon(Icons.group, size: 14, color: Colors.grey.shade600),
                                    const SizedBox(width: 4),
                                    Text(a.groupeNom,
                                        style: const TextStyle(
                                            color: Colors.grey, fontSize: 12)),
                                  ],
                                ),
                              ],
                            ),
                          ),
                          ElevatedButton(
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.green.shade700,
                              foregroundColor: Colors.white,
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(8)),
                            ),
                            onPressed: () {
                              _tabController.animateTo(1);
                            },
                            child: const Text('Notes', style: TextStyle(fontSize: 12)),
                          ),
                        ],
                      ),
                    );
                  },
                ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 2 : SAISIE DES NOTES
            // ════════════════════════════════════════════════
          
            AffectationModel? _selectedAffectation;
            String _selectedSemestre = 'S1';
            List _etudiants = [];
            List _notesExistantes = [];
            final Map _noteControllers = {};
            bool _loadingEtudiants = false;
            bool _saving = false;
          
            Widget _buildSaisieNotes() {
              return Column(
                children: [
                  // Sélection affectation + semestre
                  Container(
                    padding: const EdgeInsets.all(16),
                    color: Colors.green.shade50,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text('Choisir la matière et le groupe',
                            style: TextStyle(fontWeight: FontWeight.bold)),
                        const SizedBox(height: 8),
                        DropdownButtonFormField(
                          decoration: InputDecoration(
                            filled: true, fillColor: Colors.white,
                            border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(10)),
                            contentPadding: const EdgeInsets.symmetric(
                                horizontal: 12, vertical: 10),
                          ),
                          hint: const Text('Matière — Groupe'),
                          value: _selectedAffectation,
                          items: _affectations.map((a) => DropdownMenuItem(
                            value: a,
                            child: Text('${a.matiereNom} — ${a.groupeNom}'),
                          )).toList(),
                          onChanged: (v) {
                            setState(() => _selectedAffectation = v);
                            if (v != null) _loadEtudiants(v);
                          },
                        ),
                        const SizedBox(height: 10),
                        Row(
                          children: ['S1', 'S2'].map((s) => Padding(
                            padding: const EdgeInsets.only(right: 8),
                            child: ChoiceChip(
                              label: Text(s),
                              selected: _selectedSemestre == s,
                              selectedColor: Colors.green.shade700,
                              labelStyle: TextStyle(
                                color: _selectedSemestre == s
                                    ? Colors.white : Colors.black,
                              ),
                              onSelected: (_) {
                                setState(() => _selectedSemestre = s);
                                if (_selectedAffectation != null) {
                                  _loadEtudiants(_selectedAffectation!);
                                }
                              },
                            ),
                          )).toList(),
                        ),
                      ],
                    ),
                  ),
          
                  // Liste des étudiants avec champs de notes
                  Expanded(
                    child: _loadingEtudiants
                        ? const Center(child: CircularProgressIndicator())
                        : _selectedAffectation == null
                            ? Center(
                                child: Column(
                                  mainAxisAlignment: MainAxisAlignment.center,
                                  children: [
                                    Icon(Icons.touch_app,
                                        size: 64, color: Colors.grey.shade300),
                                    const SizedBox(height: 12),
                                    const Text('Sélectionnez une matière et un groupe',
                                        style: TextStyle(color: Colors.grey)),
                                  ],
                                ),
                              )
                            : _etudiants.isEmpty
                                ? Center(
                                    child: Column(
                                      mainAxisAlignment: MainAxisAlignment.center,
                                      children: [
                                        Icon(Icons.people_outline,
                                            size: 64, color: Colors.grey.shade300),
                                        const SizedBox(height: 12),
                                        const Text('Aucun étudiant dans ce groupe',
                                            style: TextStyle(color: Colors.grey)),
                                      ],
                                    ),
                                  )
                                : Column(
                                    children: [
                                      Expanded(
                                        child: ListView.builder(
                                          padding: const EdgeInsets.all(16),
                                          itemCount: _etudiants.length,
                                          itemBuilder: (_, i) {
                                            final e = _etudiants[i];
                                            _noteControllers.putIfAbsent(
                                              e.uid,
                                              () {
                                                final existing = _notesExistantes
                                                    .where((n) => n.etudiantId == e.uid)
                                                    .firstOrNull;
                                                return TextEditingController(
                                                  text: existing != null
                                                      ? existing.note.toString()
                                                      : '',
                                                );
                                              },
                                            );
                                            return Container(
                                              margin: const EdgeInsets.only(bottom: 10),
                                              padding: const EdgeInsets.symmetric(
                                                  horizontal: 14, vertical: 10),
                                              decoration: BoxDecoration(
                                                color: Colors.white,
                                                borderRadius: BorderRadius.circular(12),
                                                boxShadow: [BoxShadow(
                                                    color: Colors.black.withOpacity(0.04),
                                                    blurRadius: 6,
                                                    offset: const Offset(0, 2))],
                                              ),
                                              child: Row(
                                                children: [
                                                  CircleAvatar(
                                                    backgroundColor:
                                                        Colors.green.shade100,
                                                    child: Text(e.prenom[0],
                                                        style: TextStyle(
                                                            color: Colors.green.shade700,
                                                            fontWeight:
                                                                FontWeight.bold)),
                                                  ),
                                                  const SizedBox(width: 12),
                                                  Expanded(
                                                    child: Column(
                                                      crossAxisAlignment:
                                                          CrossAxisAlignment.start,
                                                      children: [
                                                        Text(e.fullName,
                                                            style: const TextStyle(
                                                                fontWeight:
                                                                    FontWeight.w600)),
                                                        Text(e.email,
                                                            style: const TextStyle(
                                                                color: Colors.grey,
                                                                fontSize: 11)),
                                                      ],
                                                    ),
                                                  ),
                                                  SizedBox(
                                                    width: 80,
                                                    child: TextField(
                                                      controller:
                                                          _noteControllers[e.uid],
                                                      keyboardType:
                                                          const TextInputType
                                                              .numberWithOptions(
                                                              decimal: true),
                                                      textAlign: TextAlign.center,
                                                      decoration: InputDecoration(
                                                        hintText: '/20',
                                                        filled: true,
                                                        fillColor:
                                                            Colors.green.shade50,
                                                        border: OutlineInputBorder(
                                                          borderRadius:
                                                              BorderRadius.circular(8),
                                                          borderSide: BorderSide(
                                                              color: Colors
                                                                  .green.shade300),
                                                        ),
                                                        contentPadding:
                                                            const EdgeInsets.symmetric(
                                                                vertical: 8,
                                                                horizontal: 8),
                                                      ),
                                                    ),
                                                  ),
                                                ],
                                              ),
                                            );
                                          },
                                        ),
                                      ),
                                      // Bouton enregistrer
                                      Padding(
                                        padding: const EdgeInsets.all(16),
                                        child: SizedBox(
                                          width: double.infinity,
                                          height: 50,
                                          child: ElevatedButton.icon(
                                            icon: _saving
                                                ? const SizedBox(
                                                    width: 18, height: 18,
                                                    child: CircularProgressIndicator(
                                                        color: Colors.white,
                                                        strokeWidth: 2))
                                                : const Icon(Icons.save),
                                            label: Text(_saving
                                                ? 'Enregistrement...'
                                                : 'Enregistrer toutes les notes'),
                                            style: ElevatedButton.styleFrom(
                                              backgroundColor: Colors.green.shade700,
                                              foregroundColor: Colors.white,
                                              shape: RoundedRectangleBorder(
                                                  borderRadius:
                                                      BorderRadius.circular(12)),
                                            ),
                                            onPressed: _saving ? null : _saveNotes,
                                          ),
                                        ),
                                      ),
                                    ],
                                  ),
                  ),
                ],
              );
            }
          
            Future _loadEtudiants(AffectationModel affectation) async {
              setState(() { _loadingEtudiants = true; _noteControllers.clear(); });
              try {
                final groupe = await _scolariteCtrl.getGroupes().then(
                  (list) => list.firstWhere((g) => g.id == affectation.groupeId),
                );
                final etudiants = await _scolariteCtrl.getEtudiantsGroupe(groupe);
                final notes = await _scolariteCtrl.getNotesGroupe(
                  groupeId:  affectation.groupeId,
                  matiereId: affectation.matiereId,
                  semestre:  _selectedSemestre,
                );
                setState(() {
                  _etudiants      = etudiants;
                  _notesExistantes = notes;
                });
              } finally {
                setState(() => _loadingEtudiants = false);
              }
            }
          
            Future _saveNotes() async {
              if (_selectedAffectation == null) return;
              setState(() => _saving = true);
              try {
                for (final etudiant in _etudiants) {
                  final ctrl = _noteControllers[etudiant.uid];
                  if (ctrl == null || ctrl.text.isEmpty) continue;
                  final noteVal = double.tryParse(ctrl.text);
                  if (noteVal == null || noteVal < 0 || noteVal > 20) continue;
          
                  await _scolariteCtrl.saisirNote(
                    etudiantId:  etudiant.uid,
                    etudiantNom: etudiant.fullName,
                    matiereId:   _selectedAffectation!.matiereId,
                    matiereNom:  _selectedAffectation!.matiereNom,
                    coefficient: 1, // récupérable depuis matiere si besoin
                    enseignantId:widget.currentUser.uid,
                    groupeId:    _selectedAffectation!.groupeId,
                    note:        noteVal,
                    semestre:    _selectedSemestre,
                  );
                }
                if (mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: const Text('✅ Notes enregistrées avec succès !'),
                      backgroundColor: Colors.green.shade700,
                    ),
                  );
                }
              } catch (e) {
                if (mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Erreur: $e'),
                        backgroundColor: Colors.red),
                  );
                }
              } finally {
                setState(() => _saving = false);
              }
            }
          }
      • ? Note : Si une note existe déjà pour un étudiant/matière/semestre, saisirNote() la met à jour au lieu d'en créer une nouvelle. Les champs sont pré-remplis avec les valeurs existantes.
    3. Fichier views/etudiant/etudiant_dashboard.dart
      • Le dashboard étudiant contient 2 onglets: son profil et son bulletin :
        • Onglet Mon profil: nom, email, groupe affecté, nombre de notes enregistrées
        • Onglet Mon bulletin: toggle S1/S2 → liste des notes avec barre de progression colorée + carte moyenne générale + statut admis ✅ / ajourné ❌
          // File: lib/views/etudiant/etudiant_dashboard.dart (extrait)
           
          // Chargement des notes et du groupe de l'étudiant connecté
          // File: lib/views/etudiant/etudiant_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/scolarite_controller.dart';
          import '../../models/user_model.dart';
          import '../../models/note_model.dart';
          import '../../models/groupe_model.dart';
          import '../login_view.dart';
          /// ? DASHBOARD ÉTUDIANT
          /// Fonctionnalités: Voir son groupe, son bulletin par semestre
          class EtudiantDashboard extends StatefulWidget {
            final UserModel currentUser;
            const EtudiantDashboard({super.key, required this.currentUser});
          
            @override
            State createState() => _EtudiantDashboardState();
          }
          
          class _EtudiantDashboardState extends State
              with SingleTickerProviderStateMixin {
            final _scolariteCtrl = ScolariteController();
            final _authController      = AuthController();
            late TabController _tabController;
          
            List _notes     = [];
            GroupeModel?    _groupe;
            bool            _loading   = true;
            String          _semestre  = 'S1';
          
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadData();
            }
          
            @override
            void dispose() {
              _tabController.dispose();
              super.dispose();
            }
          
            Future _loadData() async {
              setState(() => _loading = true);
              try {
                final results = await Future.wait([
                  _scolariteCtrl.getNotesEtudiant(widget.currentUser.uid),
                  _scolariteCtrl.getGroupeEtudiant(widget.currentUser.uid),
                ]);
                setState(() {
                  _notes  = results[0] as List;
                  _groupe = results[1] as GroupeModel?;
                });
              } finally {
                setState(() => _loading = false);
              }
            }
          
            void _logout() async {
            await _authController.signOut();
            if (!mounted) return;
            Navigator.of(context).pushReplacement(
              MaterialPageRoute(builder: (_) => const LoginView()),
            );
          }
          
            List get _notesSemestre =>
                _notes.where((n) => n.semestre == _semestre).toList();
          
            double get _moyenne =>
                _scolariteCtrl.calculerMoyenne(_notes, _semestre);
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                backgroundColor: const Color(0xFFF3F4F6),
                appBar: AppBar(
                  backgroundColor: Colors.blue.shade700,
                  foregroundColor: Colors.white,
                  title: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Espace Étudiant',
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                      Text(widget.currentUser.fullName,
                          style: const TextStyle(fontSize: 12, color: Colors.white70)),
                    ],
                  ),
                  leading: Container(
                    margin: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.white.withOpacity(0.2),
                      shape: BoxShape.circle,
                    ),
                    child: const Icon(Icons.school, color: Colors.white),
                  ),
                  actions: [
                    IconButton(icon: const Icon(Icons.logout_rounded), onPressed: _logout),
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    unselectedLabelColor: Colors.white60,
                    tabs: const [
                      Tab(icon: Icon(Icons.info_outline),   text: 'Mon profil'),
                      Tab(icon: Icon(Icons.grade_outlined),  text: 'Mon bulletin'),
                    ],
                  ),
                ),
                body: _loading
                    ? const Center(child: CircularProgressIndicator())
                    : TabBarView(
                        controller: _tabController,
                        children: [
                          _buildProfil(),
                          _buildBulletin(),
                        ],
                      ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 1 : MON PROFIL
            // ════════════════════════════════════════════════
          
            Widget _buildProfil() {
              return RefreshIndicator(
                onRefresh: _loadData,
                child: ListView(
                  padding: const EdgeInsets.all(20),
                  children: [
                    // Carte identité
                    Container(
                      padding: const EdgeInsets.all(20),
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: [Colors.blue.shade700, Colors.blue.shade500],
                        ),
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: Row(
                        children: [
                          CircleAvatar(
                            radius: 32,
                            backgroundColor: Colors.white.withOpacity(0.2),
                            child: Text(
                              widget.currentUser.prenom[0],
                              style: const TextStyle(
                                  fontSize: 28, color: Colors.white,
                                  fontWeight: FontWeight.bold),
                            ),
                          ),
                          const SizedBox(width: 16),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(widget.currentUser.fullName,
                                    style: const TextStyle(
                                        color: Colors.white, fontSize: 20,
                                        fontWeight: FontWeight.bold)),
                                Text(widget.currentUser.email,
                                    style: const TextStyle(
                                        color: Colors.white70, fontSize: 13)),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(height: 20),
          
                    // Groupe
                    _infoTile(
                      icon: Icons.group,
                      color: Colors.deepPurple,
                      label: 'Groupe',
                      value: _groupe != null
                          ? '${_groupe!.nom} — ${_groupe!.niveau}'
                          : 'Non affecté à un groupe',
                    ),
                    _infoTile(
                      icon: Icons.school,
                      color: Colors.blue,
                      label: 'Statut',
                      value: 'Étudiant',
                    ),
                    _infoTile(
                      icon: Icons.grade,
                      color: Colors.orange,
                      label: 'Notes enregistrées',
                      value: '${_notes.length} note(s)',
                    ),
                  ],
                ),
              );
            }
          
            Widget _infoTile({
              required IconData icon,
              required Color color,
              required String label,
              required String value,
            }) {
              return Container(
                margin: const EdgeInsets.only(bottom: 12),
                padding: const EdgeInsets.all(14),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(12),
                  boxShadow: [BoxShadow(
                      color: Colors.black.withOpacity(0.05),
                      blurRadius: 6, offset: const Offset(0, 2))],
                ),
                child: Row(
                  children: [
                    Container(
                      width: 40, height: 40,
                      decoration: BoxDecoration(
                        color: color.withOpacity(0.12),
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: Icon(icon, color: color, size: 20),
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(label,
                              style: const TextStyle(
                                  color: Colors.grey, fontSize: 12)),
                          Text(value,
                              style: const TextStyle(
                                  fontWeight: FontWeight.w600, fontSize: 14)),
                        ],
                      ),
                    ),
                  ],
                ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 2 : BULLETIN
            // ════════════════════════════════════════════════
          
            Widget _buildBulletin() {
              return Column(
                children: [
                  // Sélection semestre + carte moyenne
                  Container(
                    padding: const EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.blue.shade700,
                    ),
                    child: Column(
                      children: [
                        // Toggle semestre
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: ['S1', 'S2'].map((s) => Padding(
                            padding: const EdgeInsets.symmetric(horizontal: 4),
                            child: ChoiceChip(
                              label: Text(s,
                                  style: TextStyle(
                                    color: _semestre == s
                                        ? Colors.blue.shade700
                                        : Colors.white,
                                    fontWeight: FontWeight.bold,
                                  )),
                              selected: _semestre == s,
                              selectedColor: Colors.white,
                              backgroundColor: Colors.blue.shade500,
                              onSelected: (_) =>
                                  setState(() => _semestre = s),
                            ),
                          )).toList(),
                        ),
                        const SizedBox(height: 12),
                        // Carte moyenne
                        Container(
                          padding: const EdgeInsets.all(16),
                          decoration: BoxDecoration(
                            color: Colors.white.withOpacity(0.15),
                            borderRadius: BorderRadius.circular(12),
                          ),
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceAround,
                            children: [
                              Column(
                                children: [
                                  Text(
                                    _moyenne.toStringAsFixed(2),
                                    style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 36,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                  const Text('Moyenne générale',
                                      style: TextStyle(
                                          color: Colors.white70, fontSize: 12)),
                                ],
                              ),
                              Container(
                                width: 1, height: 50,
                                color: Colors.white30,
                              ),
                              Column(
                                children: [
                                  Text(
                                    '${_notesSemestre.length}',
                                    style: const TextStyle(
                                        color: Colors.white,
                                        fontSize: 36,
                                        fontWeight: FontWeight.bold),
                                  ),
                                  const Text('Matières notées',
                                      style: TextStyle(
                                          color: Colors.white70, fontSize: 12)),
                                ],
                              ),
                              Container(
                                width: 1, height: 50,
                                color: Colors.white30,
                              ),
                              Column(
                                children: [
                                  Text(
                                    _moyenne >= 10 ? '✅' : '❌',
                                    style: const TextStyle(fontSize: 30),
                                  ),
                                  Text(
                                    _moyenne >= 10 ? 'Admis' : 'Ajourné',
                                    style: const TextStyle(
                                        color: Colors.white70, fontSize: 12),
                                  ),
                                ],
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
          
                  // Liste des notes
                  Expanded(
                    child: _notesSemestre.isEmpty
                        ? Center(
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                Icon(Icons.grade_outlined,
                                    size: 64, color: Colors.grey.shade300),
                                const SizedBox(height: 12),
                                Text(
                                  'Aucune note pour le semestre $_semestre',
                                  style: const TextStyle(
                                      color: Colors.grey, fontSize: 16),
                                ),
                              ],
                            ),
                          )
                        : RefreshIndicator(
                            onRefresh: _loadData,
                            child: ListView.builder(
                              padding: const EdgeInsets.all(16),
                              itemCount: _notesSemestre.length,
                              itemBuilder: (_, i) {
                                final n = _notesSemestre[i];
                                final noteColor = n.note >= 10
                                    ? Colors.green.shade700
                                    : n.note >= 8
                                        ? Colors.orange.shade700
                                        : Colors.red.shade700;
                                return Container(
                                  margin: const EdgeInsets.only(bottom: 10),
                                  padding: const EdgeInsets.all(14),
                                  decoration: BoxDecoration(
                                    color: Colors.white,
                                    borderRadius: BorderRadius.circular(12),
                                    boxShadow: [BoxShadow(
                                        color: Colors.black.withOpacity(0.05),
                                        blurRadius: 6,
                                        offset: const Offset(0, 2))],
                                  ),
                                  child: Row(
                                    children: [
                                      Container(
                                        width: 50, height: 50,
                                        decoration: BoxDecoration(
                                          color: noteColor.withOpacity(0.1),
                                          borderRadius: BorderRadius.circular(10),
                                        ),
                                        child: Center(
                                          child: Text(
                                            n.note.toStringAsFixed(1),
                                            style: TextStyle(
                                                color: noteColor,
                                                fontWeight: FontWeight.bold,
                                                fontSize: 16),
                                          ),
                                        ),
                                      ),
                                      const SizedBox(width: 14),
                                      Expanded(
                                        child: Column(
                                          crossAxisAlignment: CrossAxisAlignment.start,
                                          children: [
                                            Text(n.matiereNom,
                                                style: const TextStyle(
                                                    fontWeight: FontWeight.bold,
                                                    fontSize: 14)),
                                            Text('Coeff. ${n.coefficient}',
                                                style: const TextStyle(
                                                    color: Colors.grey,
                                                    fontSize: 12)),
                                          ],
                                        ),
                                      ),
                                      // Barre de progression
                                      SizedBox(
                                        width: 80,
                                        child: Column(
                                          crossAxisAlignment: CrossAxisAlignment.end,
                                          children: [
                                            Text('${n.note}/20',
                                                style: TextStyle(
                                                    color: noteColor,
                                                    fontWeight: FontWeight.bold)),
                                            const SizedBox(height: 4),
                                            LinearProgressIndicator(
                                              value: n.note / 20,
                                              backgroundColor: Colors.grey.shade200,
                                              valueColor:
                                                  AlwaysStoppedAnimation(noteColor),
                                              borderRadius: BorderRadius.circular(4),
                                            ),
                                          ],
                                        ),
                                      ),
                                    ],
                                  ),
                                );
                              },
                            ),
                          ),
                  ),
                ],
              );
            }
          }
          
          
    4. Fichier views/admin/admin_dashboard.dart
      • Le dashboard admin est mis à jour avec un 3ème onglet Affectations, car l'admin peut également affecter des enseignants aux matières en plus du responsable :
          // File: lib/views/admin/admin_dashboard.dart (extrait)
           
          // File: lib/views/admin/admin_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../controllers/scolarite_controller.dart';
          import '../../models/user_model.dart';
          import '../../models/affectation_model.dart';
          import '../../models/matiere_model.dart';
          import '../../models/groupe_model.dart';
          import '../login_view.dart';
          /// ? DASHBOARD ADMINISTRATEUR
          /// Fonctionnalités: Créer comptes, voir utilisateurs, affecter enseignants
          class AdminDashboard extends StatefulWidget {
            final UserModel currentUser;
            const AdminDashboard({super.key, required this.currentUser});
          
            @override
            State createState() => _AdminDashboardState();
          }
          
          class _AdminDashboardState extends State
              with SingleTickerProviderStateMixin {
            final _userController     = UserController();
            final _authController     = AuthController();
            final _scolariteCtrl      = ScolariteController();
            late TabController _tabController;
          
            List        _users        = [];
            List _affectations = [];
            List     _matieres     = [];
            List      _groupes      = [];
            List        _enseignants  = [];
            bool _loadingUsers = true;
          
            // Formulaire création compte
            final _formKey      = GlobalKey();
            final _emailCtrl    = TextEditingController();
            final _passwordCtrl = TextEditingController();
            final _nomCtrl      = TextEditingController();
            final _prenomCtrl   = TextEditingController();
            UserRole _selectedRole = UserRole.etudiant;
            bool    _creating   = false;
            String? _createError;
            String? _createSuccess;
          
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 3, vsync: this);
              _loadAll();
            }
          
            @override
            void dispose() {
              _tabController.dispose();
              _emailCtrl.dispose();
              _passwordCtrl.dispose();
              _nomCtrl.dispose();
              _prenomCtrl.dispose();
              super.dispose();
            }
          
            Future _loadAll() async {
              setState(() => _loadingUsers = true);
              try {
                final results = await Future.wait([
                  _userController.getAllUsers(),
                  _scolariteCtrl.getAffectations(),
                  _scolariteCtrl.getMatieres(),
                  _scolariteCtrl.getGroupes(),
                  _userController.getUsersByRole(UserRole.enseignant),
                ]);
                setState(() {
                  _users        = results[0] as List;
                  _affectations = results[1] as List;
                  _matieres     = results[2] as List;
                  _groupes      = results[3] as List;
                  _enseignants  = results[4] as List;
                });
              } finally {
                setState(() => _loadingUsers = false);
              }
            }
          
            Future _createUser() async {
              if (!_formKey.currentState!.validate()) return;
              setState(() { _creating = true; _createError = null; _createSuccess = null; });
              try {
                await _userController.createUser(
                  email:    _emailCtrl.text,
                  password: _passwordCtrl.text,
                  nom:      _nomCtrl.text,
                  prenom:   _prenomCtrl.text,
                  role:     _selectedRole,
                );
                setState(() => _createSuccess =
                    '✅ Compte créé : ${_prenomCtrl.text} ${_nomCtrl.text} (${_selectedRole.displayName})');
                _formKey.currentState!.reset();
                _emailCtrl.clear(); _passwordCtrl.clear();
                _nomCtrl.clear();   _prenomCtrl.clear();
                _loadAll();
              } catch (e) {
                setState(() => _createError = e.toString());
              } finally {
                setState(() => _creating = false);
              }
            }
          
            void _logout() async {
            await _authController.signOut();
            if (!mounted) return;
            Navigator.of(context).pushReplacement(
              MaterialPageRoute(builder: (_) => const LoginView()),
            );
          }
          
            Color _roleColor(UserRole role) {
              switch (role) {
                case UserRole.admin:               return Colors.red;
                case UserRole.enseignant:          return Colors.green;
                case UserRole.etudiant:            return Colors.blue;
                case UserRole.responsable_etudiant:return Colors.orange;
              }
            }
          
            IconData _roleIcon(UserRole role) {
              switch (role) {
                case UserRole.admin:               return Icons.admin_panel_settings;
                case UserRole.enseignant:          return Icons.cast_for_education;
                case UserRole.etudiant:            return Icons.school;
                case UserRole.responsable_etudiant:return Icons.supervisor_account;
              }
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                backgroundColor: const Color(0xFFF3F4F6),
                appBar: AppBar(
                  backgroundColor: Colors.red.shade700,
                  foregroundColor: Colors.white,
                  title: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Dashboard Admin',
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                      Text('Bonjour, ${widget.currentUser.fullName}',
                          style: const TextStyle(fontSize: 12, color: Colors.white70)),
                    ],
                  ),
                  leading: Container(
                    margin: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.white.withOpacity(0.2),
                      shape: BoxShape.circle,
                    ),
                    child: const Icon(Icons.admin_panel_settings, color: Colors.white),
                  ),
                  actions: [
                    IconButton(icon: const Icon(Icons.logout_rounded), onPressed: _logout),
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    unselectedLabelColor: Colors.white60,
                    tabs: const [
                      Tab(icon: Icon(Icons.person_add),  text: 'Créer compte'),
                      Tab(icon: Icon(Icons.people),       text: 'Utilisateurs'),
                      Tab(icon: Icon(Icons.link),         text: 'Affectations'),
                    ],
                  ),
                ),
                body: _loadingUsers
                    ? const Center(child: CircularProgressIndicator())
                    : TabBarView(
                        controller: _tabController,
                        children: [
                          _buildCreateUserTab(),
                          _buildUsersListTab(),
                          _buildAffectationsTab(),
                        ],
                      ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 1 : CRÉER UN COMPTE
            // ════════════════════════════════════════════════
          
            Widget _buildCreateUserTab() {
              return SingleChildScrollView(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: [
                    Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Colors.red.shade50,
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(color: Colors.red.shade200),
                      ),
                      child: const Row(
                        children: [
                          Icon(Icons.info_outline, color: Colors.red),
                          SizedBox(width: 10),
                          Expanded(
                            child: Text(
                              'En tant qu\'admin, vous créez tous les comptes avec email, mot de passe et rôle.',
                              style: TextStyle(color: Colors.red, fontSize: 13),
                            ),
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(height: 20),
                    Container(
                      padding: const EdgeInsets.all(20),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(16),
                        boxShadow: [BoxShadow(
                            color: Colors.black.withOpacity(0.06),
                            blurRadius: 10, offset: const Offset(0, 4))],
                      ),
                      child: Form(
                        key: _formKey,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            const Text('Nouveau compte',
                                style: TextStyle(fontWeight: FontWeight.bold,
                                    fontSize: 18, color: Color(0xFF1A237E))),
                            const SizedBox(height: 20),
                            Row(
                              children: [
                                Expanded(child: _field(_prenomCtrl, 'Prénom', Icons.person_outline)),
                                const SizedBox(width: 12),
                                Expanded(child: _field(_nomCtrl, 'Nom', Icons.person)),
                              ],
                            ),
                            const SizedBox(height: 14),
                            _field(_emailCtrl, 'Email', Icons.email_outlined,
                                keyboardType: TextInputType.emailAddress),
                            const SizedBox(height: 14),
                            _field(_passwordCtrl, 'Mot de passe', Icons.lock_outline,
                                obscure: true, hint: 'Min. 6 caractères'),
                            const SizedBox(height: 14),
                            const Text('Rôle attribué',
                                style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
                            const SizedBox(height: 8),
                            Wrap(
                              spacing: 8, runSpacing: 8,
                              children: UserRole.values.map((role) {
                                final selected = _selectedRole == role;
                                return GestureDetector(
                                  onTap: () => setState(() => _selectedRole = role),
                                  child: Container(
                                    padding: const EdgeInsets.symmetric(
                                        horizontal: 14, vertical: 10),
                                    decoration: BoxDecoration(
                                      color: selected
                                          ? _roleColor(role).withOpacity(0.15)
                                          : Colors.grey.shade100,
                                      borderRadius: BorderRadius.circular(10),
                                      border: Border.all(
                                        color: selected ? _roleColor(role) : Colors.grey.shade300,
                                        width: selected ? 2 : 1,
                                      ),
                                    ),
                                    child: Row(
                                      mainAxisSize: MainAxisSize.min,
                                      children: [
                                        Icon(_roleIcon(role),
                                            color: selected ? _roleColor(role) : Colors.grey,
                                            size: 18),
                                        const SizedBox(width: 6),
                                        Text(role.displayName,
                                            style: TextStyle(
                                              color: selected ? _roleColor(role) : Colors.grey.shade700,
                                              fontWeight: selected ? FontWeight.bold : FontWeight.normal,
                                              fontSize: 13,
                                            )),
                                      ],
                                    ),
                                  ),
                                );
                              }).toList(),
                            ),
                            const SizedBox(height: 20),
                            if (_createError   != null) _feedbackBox(_createError!,   isError: true),
                            if (_createSuccess != null) _feedbackBox(_createSuccess!, isError: false),
                            const SizedBox(height: 12),
                            SizedBox(
                              width: double.infinity, height: 50,
                              child: ElevatedButton.icon(
                                onPressed: _creating ? null : _createUser,
                                icon: _creating
                                    ? const SizedBox(width: 18, height: 18,
                                        child: CircularProgressIndicator(
                                            color: Colors.white, strokeWidth: 2))
                                    : const Icon(Icons.person_add),
                                label: Text(_creating ? 'Création...' : 'Créer le compte'),
                                style: ElevatedButton.styleFrom(
                                  backgroundColor: Colors.red.shade700,
                                  foregroundColor: Colors.white,
                                  shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(12)),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 2 : LISTE DES UTILISATEURS
            // ════════════════════════════════════════════════
          
            Widget _buildUsersListTab() {
              final stats = {};
              for (final u in _users) {
                stats[u.role] = (stats[u.role] ?? 0) + 1;
              }
          
              return RefreshIndicator(
                onRefresh: _loadAll,
                child: ListView(
                  padding: const EdgeInsets.all(16),
                  children: [
                    Row(
                      children: UserRole.values.map((role) => Expanded(
                        child: Container(
                          margin: const EdgeInsets.symmetric(horizontal: 4),
                          padding: const EdgeInsets.all(10),
                          decoration: BoxDecoration(
                            color: _roleColor(role).withOpacity(0.1),
                            borderRadius: BorderRadius.circular(10),
                            border: Border.all(color: _roleColor(role).withOpacity(0.3)),
                          ),
                          child: Column(
                            children: [
                              Icon(_roleIcon(role), color: _roleColor(role), size: 20),
                              const SizedBox(height: 4),
                              Text('${stats[role] ?? 0}',
                                  style: TextStyle(fontWeight: FontWeight.bold,
                                      fontSize: 18, color: _roleColor(role))),
                              Text(
                                role == UserRole.responsable_etudiant ? 'Resp.' : role.displayName,
                                style: const TextStyle(fontSize: 9, color: Colors.grey),
                                textAlign: TextAlign.center,
                              ),
                            ],
                          ),
                        ),
                      )).toList(),
                    ),
                    const SizedBox(height: 16),
                    ..._users.map((user) => Container(
                      margin: const EdgeInsets.only(bottom: 10),
                      padding: const EdgeInsets.all(14),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(12),
                        boxShadow: [BoxShadow(
                            color: Colors.black.withOpacity(0.05),
                            blurRadius: 6, offset: const Offset(0, 2))],
                      ),
                      child: Row(
                        children: [
                          Container(
                            width: 44, height: 44,
                            decoration: BoxDecoration(
                              color: _roleColor(user.role).withOpacity(0.15),
                              shape: BoxShape.circle,
                            ),
                            child: Icon(_roleIcon(user.role),
                                color: _roleColor(user.role), size: 22),
                          ),
                          const SizedBox(width: 12),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(user.fullName,
                                    style: const TextStyle(
                                        fontWeight: FontWeight.w600, fontSize: 14)),
                                Text(user.email,
                                    style: const TextStyle(
                                        color: Colors.grey, fontSize: 12)),
                                const SizedBox(height: 4),
                                Container(
                                  padding: const EdgeInsets.symmetric(
                                      horizontal: 8, vertical: 2),
                                  decoration: BoxDecoration(
                                    color: _roleColor(user.role).withOpacity(0.12),
                                    borderRadius: BorderRadius.circular(20),
                                  ),
                                  child: Text(user.role.displayName,
                                      style: TextStyle(
                                          color: _roleColor(user.role),
                                          fontSize: 11,
                                          fontWeight: FontWeight.w600)),
                                ),
                              ],
                            ),
                          ),
                          IconButton(
                            icon: const Icon(Icons.delete_outline, color: Colors.red),
                            onPressed: () async {
                              await _userController.deleteUserFromFirestore(user.uid);
                              _loadAll();
                            },
                          ),
                        ],
                      ),
                    )),
                  ],
                ),
              );
            }
          
            // ════════════════════════════════════════════════
            // ONGLET 3 : AFFECTATIONS (admin aussi)
            // ════════════════════════════════════════════════
          
            Widget _buildAffectationsTab() {
              return Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.all(16),
                    child: ElevatedButton.icon(
                      icon: const Icon(Icons.add),
                      label: const Text('Nouvelle affectation'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.red.shade700,
                        foregroundColor: Colors.white,
                        minimumSize: const Size(double.infinity, 48),
                        shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(12)),
                      ),
                      onPressed: () => _dialogAffectation(),
                    ),
                  ),
                  Expanded(
                    child: _affectations.isEmpty
                        ? Center(
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                Icon(Icons.link_off, size: 64, color: Colors.grey.shade300),
                                const SizedBox(height: 12),
                                const Text('Aucune affectation',
                                    style: TextStyle(color: Colors.grey)),
                              ],
                            ),
                          )
                        : ListView.builder(
                            padding: const EdgeInsets.symmetric(horizontal: 16),
                            itemCount: _affectations.length,
                            itemBuilder: (_, i) {
                              final a = _affectations[i];
                              return Container(
                                margin: const EdgeInsets.only(bottom: 10),
                                padding: const EdgeInsets.all(14),
                                decoration: BoxDecoration(
                                  color: Colors.white,
                                  borderRadius: BorderRadius.circular(12),
                                  boxShadow: [BoxShadow(
                                      color: Colors.black.withOpacity(0.05),
                                      blurRadius: 6, offset: const Offset(0, 2))],
                                ),
                                child: Row(
                                  children: [
                                    Container(
                                      width: 42, height: 42,
                                      decoration: BoxDecoration(
                                        color: Colors.teal.withOpacity(0.12),
                                        borderRadius: BorderRadius.circular(10),
                                      ),
                                      child: const Icon(Icons.link,
                                          color: Colors.teal, size: 22),
                                    ),
                                    const SizedBox(width: 12),
                                    Expanded(
                                      child: Column(
                                        crossAxisAlignment: CrossAxisAlignment.start,
                                        children: [
                                          Text('${a.matiereNom} → ${a.enseignantNom}',
                                              style: const TextStyle(
                                                  fontWeight: FontWeight.w600,
                                                  fontSize: 14)),
                                          Text('Groupe : ${a.groupeNom}',
                                              style: const TextStyle(
                                                  color: Colors.grey, fontSize: 12)),
                                        ],
                                      ),
                                    ),
                                    IconButton(
                                      icon: const Icon(Icons.delete_outline,
                                          color: Colors.red),
                                      onPressed: () async {
                                        await _scolariteCtrl.deleteAffectation(a.id);
                                        _loadAll();
                                      },
                                    ),
                                  ],
                                ),
                              );
                            },
                          ),
                  ),
                ],
              );
            }
          
            Future _dialogAffectation() async {
              UserModel?    selectedEnseignant;
              MatiereModel? selectedMatiere;
              GroupeModel?  selectedGroupe;
          
              await showDialog(
                context: context,
                builder: (ctx) => StatefulBuilder(
                  builder: (ctx, setStateDialog) => AlertDialog(
                    title: const Text('Nouvelle affectation'),
                    content: SingleChildScrollView(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text('Enseignant',
                              style: TextStyle(fontWeight: FontWeight.bold)),
                          DropdownButton(
                            isExpanded: true,
                            hint: const Text('Choisir un enseignant'),
                            value: selectedEnseignant,
                            items: _enseignants.map((e) => DropdownMenuItem(
                              value: e, child: Text(e.fullName),
                            )).toList(),
                            onChanged: (v) =>
                                setStateDialog(() => selectedEnseignant = v),
                          ),
                          const SizedBox(height: 12),
                          const Text('Matière',
                              style: TextStyle(fontWeight: FontWeight.bold)),
                          DropdownButton(
                            isExpanded: true,
                            hint: const Text('Choisir une matière'),
                            value: selectedMatiere,
                            items: _matieres.map((m) => DropdownMenuItem(
                              value: m, child: Text(m.nom),
                            )).toList(),
                            onChanged: (v) =>
                                setStateDialog(() => selectedMatiere = v),
                          ),
                          const SizedBox(height: 12),
                          const Text('Groupe',
                              style: TextStyle(fontWeight: FontWeight.bold)),
                          DropdownButton(
                            isExpanded: true,
                            hint: const Text('Choisir un groupe'),
                            value: selectedGroupe,
                            items: _groupes.map((g) => DropdownMenuItem(
                              value: g, child: Text(g.displayName),
                            )).toList(),
                            onChanged: (v) =>
                                setStateDialog(() => selectedGroupe = v),
                          ),
                        ],
                      ),
                    ),
                    actions: [
                      TextButton(
                          onPressed: () => Navigator.pop(ctx),
                          child: const Text('Annuler')),
                      ElevatedButton(
                        style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.red.shade700),
                        onPressed: () async {
                          if (selectedEnseignant == null ||
                              selectedMatiere    == null ||
                              selectedGroupe     == null) return;
                          await _scolariteCtrl.createAffectation(
                            enseignantId:  selectedEnseignant!.uid,
                            enseignantNom: selectedEnseignant!.fullName,
                            matiereId:     selectedMatiere!.id,
                            matiereNom:    selectedMatiere!.nom,
                            groupeId:      selectedGroupe!.id,
                            groupeNom:     selectedGroupe!.displayName,
                          );
                          if (ctx.mounted) Navigator.pop(ctx);
                          _loadAll();
                        },
                        child: const Text('Affecter',
                            style: TextStyle(color: Colors.white)),
                      ),
                    ],
                  ),
                ),
              );
            }
          
            // ════════════════════════════════════════════════
            // WIDGETS HELPERS
            // ════════════════════════════════════════════════
          
            TextFormField _field(
              TextEditingController ctrl, String label, IconData icon, {
              bool obscure = false, String? hint, TextInputType? keyboardType,
            }) {
              return TextFormField(
                controller: ctrl,
                obscureText: obscure,
                keyboardType: keyboardType,
                decoration: InputDecoration(
                  labelText: label, hintText: hint,
                  prefixIcon: Icon(icon, size: 20),
                  border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
                  contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
                ),
                validator: (v) {
                  if (v == null || v.isEmpty) return '$label requis';
                  if (label == 'Email' && !v.contains('@')) return 'Email invalide';
                  if (label == 'Mot de passe' && v.length < 6) return 'Min. 6 caractères';
                  return null;
                },
              );
            }
          
            Widget _feedbackBox(String msg, {required bool isError}) {
              return Container(
                margin: const EdgeInsets.only(bottom: 8),
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: isError ? Colors.red.shade50 : Colors.green.shade50,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(
                      color: isError ? Colors.red.shade200 : Colors.green.shade200),
                ),
                child: Text(msg,
                    style: TextStyle(
                        color: isError ? Colors.red : Colors.green, fontSize: 13)),
              );
            }
          }
          
          
          
  12. Correction de la déconnexion dans les dashboards

    • Dans chaque dashboard, la méthode _logout() doit rediriger vers LoginView. Ajoutez l'import et modifiez la méthode dans les 4 fichiers :
        // Ajoutez cet import en haut de chaque dashboard
        import '../login_view.dart';
         
        // ✅ Méthode _logout() correcte dans chaque dashboard
        void _logout() async {
          await _authCtrl.signOut();
          if (!mounted) return;
          Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (_) => const LoginView()),
          );
        }
  13. Mise à jour des Security Rules Firestore

    • Les Security Rules Firestore sont des règles de sécurité appliquées côté serveur par Firebase, indépendamment du code Flutter. Même si un utilisateur modifie le code de l'application, il ne pourra jamais contourner ces règles.
    • Elles répondent à la question : « Qui a le droit de lire ou d'écrire dans cette collection ? »
    • Dans notre application, les règles sont basées sur le rôle de l'utilisateur connecté, stocké dans le document Firestore users/{uid}.
    • Principe de fonctionnement
      • Avant chaque lecture ou écriture, Firebase exécute la règle correspondante.
      • Si la condition retourne true → l'accès est autorisé.
      • Si la condition retourne false → l'accès est refusé avec une erreur PERMISSION_DENIED.
      • Les fonctions isAdmin(), isEnseignant(), etc. lisent le champ role depuis Firestore en temps réel pour vérifier le rôle de l'utilisateur connecté.
    • Tableau des permissions par collection
      • Collection Lecture (read) Écriture (write)
        usersAdmin ou l'utilisateur lui-mêmeAdmin uniquement
        matieresTout utilisateur connectéAdmin ou Responsable
        groupesTout utilisateur connectéAdmin ou Responsable
        affectationsTout utilisateur connectéAdmin ou Responsable
        notesTout utilisateur connectéAdmin ou Enseignant
    • Règles complètes commentées
    • Copiez ces règles dans Firebase Console → Firestore → Règles :
        rules_version = '2';
        service cloud.firestore {
          match /databases/{database}/documents {
        
            // ════════════════════════════════════════════════════════════
            // ? FONCTIONS HELPER — réutilisées dans toutes les règles
            // ════════════════════════════════════════════════════════════
        
            // Lit le champ "role" du document Firestore de l'utilisateur connecté
            // Exemple : si uid = "abc123", lit users/abc123 → data.role
            function getUserRole() {
              return get(/databases/$(database)/documents/
                     users/$(request.auth.uid)).data.role;
            }
        
            // Vérifie que l'utilisateur est connecté ET que son rôle est "admin"
            function isAdmin() {
              return request.auth != null && getUserRole() == 'admin';
            }
        
            // Vérifie que l'utilisateur est connecté ET que son rôle est "enseignant"
            function isEnseignant() {
              return request.auth != null && getUserRole() == 'enseignant';
            }
        
            // Vérifie que l'utilisateur est connecté ET que son rôle est "responsable_etudiant"
            function isResponsable() {
              return request.auth != null && getUserRole() == 'responsable_etudiant';
            }
        
            // Vérifie simplement que l'utilisateur est connecté (peu importe le rôle)
            function isAuth() {
              return request.auth != null;
            }
        
            // ════════════════════════════════════════════════════════════
            // ? COLLECTION : users
            // Contient les profils et rôles de tous les utilisateurs
            // ════════════════════════════════════════════════════════════
            match /users/{userId} {
        
              //  Lecture autorisée si :
              //    - l'utilisateur est admin (peut voir tous les profils)
              //    - OU l'utilisateur lit son propre document (request.auth.uid == userId)
              allow read: if isAuth() && (isAdmin() || request.auth.uid == userId);
        
              //  Écriture (create, update, delete) : admin uniquement
              //    Seul l'admin peut créer, modifier ou supprimer des comptes
              allow write: if isAdmin();
            }
        
            // ════════════════════════════════════════════════════════════
            // ? COLLECTION : matieres
            // Contient les matières créées par le responsable étudiant
            // ════════════════════════════════════════════════════════════
            match /matieres/{id} {
        
              //  Lecture : tout utilisateur connecté peut voir les matières
              //    (enseignants, étudiants, responsable, admin)
              allow read: if isAuth();
        
              //  Écriture : uniquement le responsable étudiant ou l'admin
              //    Les enseignants et étudiants ne peuvent PAS modifier les matières
              allow write: if isAdmin() || isResponsable();
            }
        
            // ════════════════════════════════════════════════════════════
            // ? COLLECTION : groupes
            // Contient les groupes d'étudiants (nom, niveau, liste UIDs)
            // ════════════════════════════════════════════════════════════
            match /groupes/{id} {
        
              //  Lecture : tout utilisateur connecté peut voir les groupes
              //    (nécessaire pour l'enseignant qui charge son groupe)
              allow read: if isAuth();
        
              //  Écriture : uniquement le responsable ou l'admin
              //    Cela inclut l'ajout/retrait d'un étudiant dans un groupe
              allow write: if isAdmin() || isResponsable();
            }
        
            // ════════════════════════════════════════════════════════════
            // ? COLLECTION : affectations
            // Lie un enseignant à une matière et un groupe
            // ════════════════════════════════════════════════════════════
            match /affectations/{id} {
        
              //  Lecture : tout utilisateur connecté
              //    L'enseignant doit pouvoir lire ses propres affectations
              allow read: if isAuth();
        
              //  Écriture : admin ou responsable étudiant
              //    Les deux peuvent créer ou supprimer des affectations
              allow write: if isAdmin() || isResponsable();
            }
        
            // ════════════════════════════════════════════════════════════
        //  COLLECTION : notes
        // Contient les notes saisies par les enseignants
        // ════════════════════════════════════════════════════════════
        match /notes/{id} {
        
          //  Lecture selon le rôle :
          //    - Admin       → voit toutes les notes
          //    - Responsable → voit toutes les notes (supervision)
          //    - Enseignant  → voit uniquement les notes qu'il a saisies
          //    - Étudiant    → voit UNIQUEMENT ses propres notes (etudiantId == uid)
          allow read: if isAdmin()
                   || isResponsable()
                   || (isEnseignant()
                       && resource.data.enseignantId == request.auth.uid)
                   || (isAuth()
                       && resource.data.etudiantId == request.auth.uid);
        
          //  Écriture : enseignant ou admin uniquement
          //    Un étudiant ne peut PAS modifier ses propres notes
          //    Un responsable ne peut PAS saisir de notes
          allow write: if isAdmin() || isEnseignant();
        }
        }
        }
    • ⚠️ Remarque importante : La règle allow read: if isAuth() sur les notes donne accès à toutes les notes à tout utilisateur connecté. Pour restreindre un étudiant à ses seules notes, remplacez par :
        // Restriction avancée : l'étudiant ne voit que ses propres notes
        match /notes/{id} {
          allow read: if isAdmin() || isEnseignant() || isResponsable()
                      || (isAuth() && resource.data.etudiantId == request.auth.uid);
          allow write: if isAdmin() || isEnseignant();
        }
  14. Responsable
      // File: lib/views/responsable/responsable_dashboard.dart
      import 'package:flutter/material.dart';
      import '../../controllers/auth_controller.dart';
      import '../../controllers/user_controller.dart';
      import '../../controllers/scolarite_controller.dart';
      import '../../models/user_model.dart';
      import '../../models/matiere_model.dart';
      import '../../models/groupe_model.dart';
      import '../../models/affectation_model.dart';
      import '../login_view.dart';
      /// ? DASHBOARD RESPONSABLE ÉTUDIANT
      /// Fonctionnalités: Matières, Groupes, Affectations, Étudiants
      class ResponsableDashboard extends StatefulWidget {
        final UserModel currentUser;
        const ResponsableDashboard({super.key, required this.currentUser});
      
        @override
        State createState() => _ResponsableDashboardState();
      }
      
      class _ResponsableDashboardState extends State
          with SingleTickerProviderStateMixin {
        final _scolariteCtrl = ScolariteController();
        final _userCtrl      = UserController();
        final _authController      = AuthController();
        late TabController _tabController;
      
        List    _matieres     = [];
        List     _groupes      = [];
        List_affectations = [];
        List       _etudiants    = [];
        List       _enseignants  = [];
      
        bool _loading = true;
      
        @override
        void initState() {
          super.initState();
          _tabController = TabController(length: 4, vsync: this);
          _loadAll();
        }
      
        @override
        void dispose() {
          _tabController.dispose();
          super.dispose();
        }
      
        Future _loadAll() async {
          setState(() => _loading = true);
          try {
            final results = await Future.wait([
              _scolariteCtrl.getMatieres(),
              _scolariteCtrl.getGroupes(),
              _scolariteCtrl.getAffectations(),
              _userCtrl.getUsersByRole(UserRole.etudiant),
              _userCtrl.getUsersByRole(UserRole.enseignant),
            ]);
            setState(() {
              _matieres     = results[0] as List;
              _groupes      = results[1] as List;
              _affectations = results[2] as List;
              _etudiants    = results[3] as List;
              _enseignants  = results[4] as List;
            });
          } finally {
            setState(() => _loading = false);
          }
        }
      
        void _logout() async {
        await _authController.signOut();
        if (!mounted) return;
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (_) => const LoginView()),
        );
      }
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            backgroundColor: const Color(0xFFF3F4F6),
            appBar: AppBar(
              backgroundColor: Colors.orange.shade700,
              foregroundColor: Colors.white,
              title: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('Responsable Étudiant',
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                  Text(widget.currentUser.fullName,
                      style: const TextStyle(fontSize: 12, color: Colors.white70)),
                ],
              ),
              leading: Container(
                margin: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.white.withOpacity(0.2),
                  shape: BoxShape.circle,
                ),
                child: const Icon(Icons.supervisor_account, color: Colors.white),
              ),
              actions: [
                IconButton(icon: const Icon(Icons.logout_rounded), onPressed: _logout),
              ],
              bottom: TabBar(
                controller: _tabController,
                indicatorColor: Colors.white,
                labelColor: Colors.white,
                unselectedLabelColor: Colors.white60,
                isScrollable: true,
                tabs: const [
                  Tab(icon: Icon(Icons.book),             text: 'Matières'),
                  Tab(icon: Icon(Icons.group),             text: 'Groupes'),
                  Tab(icon: Icon(Icons.link),              text: 'Affectations'),
                  Tab(icon: Icon(Icons.people),            text: 'Étudiants'),
                ],
              ),
            ),
            body: _loading
                ? const Center(child: CircularProgressIndicator())
                : TabBarView(
                    controller: _tabController,
                    children: [
                      _buildMatieres(),
                      _buildGroupes(),
                      _buildAffectations(),
                      _buildEtudiants(),
                    ],
                  ),
          );
        }
      
        // ════════════════════════════════════════════════
        // ONGLET 1 : MATIÈRES
        // ════════════════════════════════════════════════
      
        Widget _buildMatieres() {
          return Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(16),
                child: ElevatedButton.icon(
                  icon: const Icon(Icons.add),
                  label: const Text('Ajouter une matière'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.orange.shade700,
                    foregroundColor: Colors.white,
                    minimumSize: const Size(double.infinity, 48),
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                  ),
                  onPressed: () => _dialogMatiere(),
                ),
              ),
              Expanded(
                child: _matieres.isEmpty
                    ? _empty('Aucune matière créée', Icons.book_outlined)
                    : ListView.builder(
                        padding: const EdgeInsets.symmetric(horizontal: 16),
                        itemCount: _matieres.length,
                        itemBuilder: (_, i) {
                          final m = _matieres[i];
                          return _card(
                            icon: Icons.book,
                            color: Colors.orange,
                            title: m.nom,
                            subtitle: 'Coefficient : ${m.coefficient}',
                            onDelete: () async {
                              await _scolariteCtrl.deleteMatiere(m.id);
                              _loadAll();
                            },
                          );
                        },
                      ),
              ),
            ],
          );
        }
      
        Future _dialogMatiere() async {
          final nomCtrl   = TextEditingController();
          final coeffCtrl = TextEditingController(text: '1');
          await showDialog(
            context: context,
            builder: (ctx) => AlertDialog(
              title: const Text('Nouvelle matière'),
              content: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextField(controller: nomCtrl,
                      decoration: const InputDecoration(labelText: 'Nom de la matière')),
                  TextField(controller: coeffCtrl,
                      keyboardType: TextInputType.number,
                      decoration: const InputDecoration(labelText: 'Coefficient')),
                ],
              ),
              actions: [
                TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.orange.shade700),
                  onPressed: () async {
                    if (nomCtrl.text.isEmpty) return;
                    await _scolariteCtrl.createMatiere(
                      nom:         nomCtrl.text.trim(),
                      coefficient: double.tryParse(coeffCtrl.text) ?? 1,
                      createdBy:   widget.currentUser.uid,
                    );
                    if (ctx.mounted) Navigator.pop(ctx);
                    _loadAll();
                  },
                  child: const Text('Créer', style: TextStyle(color: Colors.white)),
                ),
              ],
            ),
          );
        }
      
        // ════════════════════════════════════════════════
        // ONGLET 2 : GROUPES
        // ════════════════════════════════════════════════
      
        Widget _buildGroupes() {
          return Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(16),
                child: ElevatedButton.icon(
                  icon: const Icon(Icons.add),
                  label: const Text('Créer un groupe'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.orange.shade700,
                    foregroundColor: Colors.white,
                    minimumSize: const Size(double.infinity, 48),
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                  ),
                  onPressed: () => _dialogGroupe(),
                ),
              ),
              Expanded(
                child: _groupes.isEmpty
                    ? _empty('Aucun groupe créé', Icons.group_outlined)
                    : ListView.builder(
                        padding: const EdgeInsets.symmetric(horizontal: 16),
                        itemCount: _groupes.length,
                        itemBuilder: (_, i) {
                          final g = _groupes[i];
                          return _card(
                            icon: Icons.group,
                            color: Colors.deepPurple,
                            title: g.nom,
                            subtitle: 'Niveau : ${g.niveau} — ${g.etudiants.length} étudiant(s)',
                            trailing: IconButton(
                              icon: const Icon(Icons.person_add, color: Colors.deepPurple),
                              tooltip: 'Affecter étudiant',
                              onPressed: () => _dialogAffecterEtudiant(g),
                            ),
                            onDelete: () async {
                              await _scolariteCtrl.deleteGroupe(g.id);
                              _loadAll();
                            },
                          );
                        },
                      ),
              ),
            ],
          );
        }
      
        Future _dialogGroupe() async {
          final nomCtrl    = TextEditingController();
          final niveauCtrl = TextEditingController();
          await showDialog(
            context: context,
            builder: (ctx) => AlertDialog(
              title: const Text('Nouveau groupe'),
              content: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextField(controller: nomCtrl,
                      decoration: const InputDecoration(labelText: 'Nom du groupe (ex: G1)')),
                  TextField(controller: niveauCtrl,
                      decoration: const InputDecoration(labelText: 'Niveau (ex: L2, M1)')),
                ],
              ),
              actions: [
                TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.orange.shade700),
                  onPressed: () async {
                    if (nomCtrl.text.isEmpty || niveauCtrl.text.isEmpty) return;
                    await _scolariteCtrl.createGroupe(
                      nom:    nomCtrl.text.trim(),
                      niveau: niveauCtrl.text.trim(),
                    );
                    if (ctx.mounted) Navigator.pop(ctx);
                    _loadAll();
                  },
                  child: const Text('Créer', style: TextStyle(color: Colors.white)),
                ),
              ],
            ),
          );
        }
      
        Future _dialogAffecterEtudiant(GroupeModel groupe) async {
          // Étudiants pas encore dans ce groupe
          final disponibles = _etudiants
              .where((e) => !groupe.etudiants.contains(e.uid))
              .toList();
      
          if (disponibles.isEmpty) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Tous les étudiants sont déjà dans un groupe')),
            );
            return;
          }
      
          await showDialog(
            context: context,
            builder: (ctx) => AlertDialog(
              title: Text('Affecter au groupe ${groupe.nom}'),
              content: SizedBox(
                width: 300,
                height: 300,
                child: ListView.builder(
                  itemCount: disponibles.length,
                  itemBuilder: (_, i) {
                    final e = disponibles[i];
                    return ListTile(
                      leading: CircleAvatar(child: Text(e.prenom[0])),
                      title: Text(e.fullName),
                      subtitle: Text(e.email),
                      onTap: () async {
                        await _scolariteCtrl.ajouterEtudiantGroupe(
                          groupeId:   groupe.id,
                          etudiantId: e.uid,
                        );
                        if (ctx.mounted) Navigator.pop(ctx);
                        _loadAll();
                      },
                    );
                  },
                ),
              ),
              actions: [
                TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Fermer')),
              ],
            ),
          );
        }
      
        // ════════════════════════════════════════════════
        // ONGLET 3 : AFFECTATIONS
        // ════════════════════════════════════════════════
      
        Widget _buildAffectations() {
          return Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(16),
                child: ElevatedButton.icon(
                  icon: const Icon(Icons.add),
                  label: const Text('Nouvelle affectation'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.orange.shade700,
                    foregroundColor: Colors.white,
                    minimumSize: const Size(double.infinity, 48),
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                  ),
                  onPressed: () => _dialogAffectation(),
                ),
              ),
              Expanded(
                child: _affectations.isEmpty
                    ? _empty('Aucune affectation', Icons.link_off)
                    : ListView.builder(
                        padding: const EdgeInsets.symmetric(horizontal: 16),
                        itemCount: _affectations.length,
                        itemBuilder: (_, i) {
                          final a = _affectations[i];
                          return _card(
                            icon: Icons.link,
                            color: Colors.teal,
                            title: '${a.matiereNom} → ${a.enseignantNom}',
                            subtitle: 'Groupe : ${a.groupeNom}',
                            onDelete: () async {
                              await _scolariteCtrl.deleteAffectation(a.id);
                              _loadAll();
                            },
                          );
                        },
                      ),
              ),
            ],
          );
        }
      
        Future _dialogAffectation() async {
          UserModel?    selectedEnseignant;
          MatiereModel? selectedMatiere;
          GroupeModel?  selectedGroupe;
      
          await showDialog(
            context: context,
            builder: (ctx) => StatefulBuilder(
              builder: (ctx, setStateDialog) => AlertDialog(
                title: const Text('Nouvelle affectation'),
                content: SingleChildScrollView(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Enseignant', style: TextStyle(fontWeight: FontWeight.bold)),
                      DropdownButton(
                        isExpanded: true,
                        hint: const Text('Choisir un enseignant'),
                        value: selectedEnseignant,
                        items: _enseignants.map((e) => DropdownMenuItem(
                          value: e,
                          child: Text(e.fullName),
                        )).toList(),
                        onChanged: (v) => setStateDialog(() => selectedEnseignant = v),
                      ),
                      const SizedBox(height: 12),
                      const Text('Matière', style: TextStyle(fontWeight: FontWeight.bold)),
                      DropdownButton(
                        isExpanded: true,
                        hint: const Text('Choisir une matière'),
                        value: selectedMatiere,
                        items: _matieres.map((m) => DropdownMenuItem(
                          value: m,
                          child: Text(m.nom),
                        )).toList(),
                        onChanged: (v) => setStateDialog(() => selectedMatiere = v),
                      ),
                      const SizedBox(height: 12),
                      const Text('Groupe', style: TextStyle(fontWeight: FontWeight.bold)),
                      DropdownButton(
                        isExpanded: true,
                        hint: const Text('Choisir un groupe'),
                        value: selectedGroupe,
                        items: _groupes.map((g) => DropdownMenuItem(
                          value: g,
                          child: Text(g.displayName),
                        )).toList(),
                        onChanged: (v) => setStateDialog(() => selectedGroupe = v),
                      ),
                    ],
                  ),
                ),
                actions: [
                  TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.orange.shade700),
                    onPressed: () async {
                      if (selectedEnseignant == null ||
                          selectedMatiere    == null ||
                          selectedGroupe     == null) return;
                      await _scolariteCtrl.createAffectation(
                        enseignantId:  selectedEnseignant!.uid,
                        enseignantNom: selectedEnseignant!.fullName,
                        matiereId:     selectedMatiere!.id,
                        matiereNom:    selectedMatiere!.nom,
                        groupeId:      selectedGroupe!.id,
                        groupeNom:     selectedGroupe!.displayName,
                      );
                      if (ctx.mounted) Navigator.pop(ctx);
                      _loadAll();
                    },
                    child: const Text('Affecter', style: TextStyle(color: Colors.white)),
                  ),
                ],
              ),
            ),
          );
        }
      
        // ════════════════════════════════════════════════
        // ONGLET 4 : ÉTUDIANTS
        // ════════════════════════════════════════════════
      
        Widget _buildEtudiants() {
          return ListView(
            padding: const EdgeInsets.all(16),
            children: [
              // Résumé
              Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Colors.orange.shade700, Colors.orange.shade500],
                  ),
                  borderRadius: BorderRadius.circular(14),
                ),
                child: Row(
                  children: [
                    const Icon(Icons.people, color: Colors.white, size: 36),
                    const SizedBox(width: 14),
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('${_etudiants.length}',
                            style: const TextStyle(
                                color: Colors.white, fontSize: 28,
                                fontWeight: FontWeight.bold)),
                        const Text('Étudiants inscrits',
                            style: TextStyle(color: Colors.white70)),
                      ],
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 16),
              ..._etudiants.map((e) {
                // Trouver son groupe
                final groupe = _groupes.firstWhere(
                  (g) => g.etudiants.contains(e.uid),
                  orElse: () => GroupeModel(id: '', nom: 'Non affecté',
                      niveau: '', etudiants: []),
                );
                return Container(
                  margin: const EdgeInsets.only(bottom: 10),
                  padding: const EdgeInsets.all(14),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(12),
                    boxShadow: [BoxShadow(
                        color: Colors.black.withOpacity(0.05),
                        blurRadius: 6, offset: const Offset(0, 2))],
                  ),
                  child: Row(
                    children: [
                      CircleAvatar(
                        backgroundColor: Colors.orange.shade100,
                        child: Text(e.prenom[0],
                            style: TextStyle(color: Colors.orange.shade700,
                                fontWeight: FontWeight.bold)),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(e.fullName,
                                style: const TextStyle(fontWeight: FontWeight.w600)),
                            Text(e.email,
                                style: const TextStyle(color: Colors.grey, fontSize: 12)),
                            Container(
                              margin: const EdgeInsets.only(top: 4),
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 8, vertical: 2),
                              decoration: BoxDecoration(
                                color: groupe.id.isEmpty
                                    ? Colors.grey.shade100
                                    : Colors.deepPurple.shade50,
                                borderRadius: BorderRadius.circular(20),
                              ),
                              child: Text(
                                groupe.id.isEmpty
                                    ? '⚠️ Non affecté'
                                    : '${groupe.nom} — ${groupe.niveau}',
                                style: TextStyle(
                                  fontSize: 11,
                                  color: groupe.id.isEmpty
                                      ? Colors.grey
                                      : Colors.deepPurple,
                                  fontWeight: FontWeight.w600,
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              }),
            ],
          );
        }
      
        // ════════════════════════════════════════════════
        // WIDGETS RÉUTILISABLES
        // ════════════════════════════════════════════════
      
        Widget _card({
          required IconData icon,
          required Color color,
          required String title,
          required String subtitle,
          required VoidCallback onDelete,
          Widget? trailing,
        }) {
          return Container(
            margin: const EdgeInsets.only(bottom: 10),
            padding: const EdgeInsets.all(14),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(12),
              boxShadow: [BoxShadow(
                  color: Colors.black.withOpacity(0.05),
                  blurRadius: 6, offset: const Offset(0, 2))],
            ),
            child: Row(
              children: [
                Container(
                  width: 42, height: 42,
                  decoration: BoxDecoration(
                    color: color.withOpacity(0.12),
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Icon(icon, color: color, size: 22),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(title,
                          style: const TextStyle(
                              fontWeight: FontWeight.w600, fontSize: 14)),
                      Text(subtitle,
                          style: const TextStyle(color: Colors.grey, fontSize: 12)),
                    ],
                  ),
                ),
                if (trailing != null) trailing,
                IconButton(
                  icon: const Icon(Icons.delete_outline, color: Colors.red),
                  onPressed: onDelete,
                ),
              ],
            ),
          );
        }
      
        Widget _empty(String msg, IconData icon) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(icon, size: 64, color: Colors.grey.shade300),
                const SizedBox(height: 12),
                Text(msg, style: const TextStyle(color: Colors.grey, fontSize: 16)),
              ],
            ),
          );
        }
      }
      
  15. Que se passe-t-il si vous n'écrivez pas les Security Rules ?
    • Le comportement dépend du mode choisi lors de la création de Firestore. Il existe deux modes :
    • Mode Production (par défaut recommandé) :
        rules_version = '2';
        service cloud.firestore {
          match /databases/{database}/documents {
            match /{document=**} {
              allow read, write: if false; // ← TOUT EST BLOQUÉ
            }
          }
        }
      • ❌ Conséquence : Personne ne peut lire ni écrire. L'application ne fonctionne pas du tout. Vous verrez l'erreur PERMISSION_DENIED partout.
    • Mode Test (souvent choisi par erreur) :
        rules_version = '2';
        service cloud.firestore {
          match /databases/{database}/documents {
            match /{document=**} {
              allow read, write: if true; // ← TOUT EST OUVERT
            }
          }
        }
      • ⚠️ Conséquence : N'importe qui peut lire, modifier ou supprimer toutes vos données sans être connecté. C'est extrêmement dangereux.
    • Résumé des risques
      • Situation Conséquence
        Mode production sans règles personnaliséesApp bloquée, rien ne fonctionne (PERMISSION_DENIED)
        Mode test sans règles personnaliséesToutes les données accessibles publiquement sur Internet
        Sans règle sur notesUn étudiant peut lire les notes de tous les autres étudiants
        Sans règle sur usersN'importe qui peut lire ou modifier les rôles de tous les utilisateurs
    • Conclusion
      • Les Security Rules sont obligatoires pour deux raisons :
        • Fonctionnement : sans elles en mode production, l'application est complètement bloquée.
        • Sécurité : sans elles en mode test, vos données sont exposées publiquement sur Internet et accessibles par n'importe qui via l'API Firestore REST, même sans votre application.
      • ? Firebase affiche d'ailleurs un avertissement rouge dans la console si votre Firestore est en mode test depuis plus de 30 jours.