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

Se connecter à Flutter avec PHP MySQL

Un cours complet pour maîtriser Se connecter à Flutter avec PHP MySQL.

0 chapitres 0 QCM Accès gratuit
 
  1. Objectif

    • À la fin de ce tutoriel, vous serez capable de :
      • Créer une base de données MySQL simple pour l’authentification et la gestion des clients.
      • Écrire un backend PHP pour interagir avec MySQL (CRUD et connexion).
      • Échanger des données entre Flutter et PHP via HTTP en utilisant le format JSON.
      • Créer une interface Flutter fonctionnelle pour se connecter, afficher, ajouter, modifier et supprimer des clients avec vérification côté serveur.
  2. Présentation

    • Flutter ne peut pas se connecter directement à une base de données MySQL. Pour cela, on utilise PHP comme intermédiaire :
      • PHP interagit avec MySQL et expose des APIs REST (GET, POST, UPDATE, DELETE).
      • Flutter envoie des requêtes HTTP vers ces APIs et reçoit des réponses JSON.
    • Cette architecture permet de sécuriser l’accès à la base de données tout en gardant la logique Flutter indépendante du backend.
  3. Règles et bonnes pratiques pour Flutter ↔ PHP MySQL

    1. Côté PHP :
      • Toujours définir les en-têtes CORS pour autoriser les requêtes Flutter :
          header("Access-Control-Allow-Origin: *"); header("Content-Type: application/json");
      • Utiliser json_decode(file_get_contents("php://input")) pour lire les données envoyées par Flutter.
      • Renvoyer systématiquement des réponses JSON avec un indicateur de succès et un message :echo json_encode(['success' => true, 'message' => 'Opération réussie']);
      • Toujours utiliser des requêtes préparées pour sécuriser contre les injections SQL.
      • Gérer la requête OPTIONS pour le pré-vol CORS sur les requêtes POST/PUT/DELETE.
    2. Côté Flutter :
      • Créer une classe modèle pour représenter les données (ex : Client).
      • Utiliser le package http pour envoyer des requêtes HTTP.
      • Convertir les réponses JSON en objets Flutter.
      • Gérer les erreurs réseau et les statuts HTTP.
      • Séparer la logique UI et la logique API via un service dédié (ex : ClientService).
    3. Architecture CRUD Flutter ↔ PHP MySQL
        Action PHP API Flutter
        Lire get_client.php → renvoie JSON FutureBuilder pour afficher la liste
        Ajouter insert_client.php (POST JSON) Formulaire → ClientService.insertClient()
        Modifier update_client.php (POST JSON) Formulaire pré-rempli → ClientService.updateClient()
        Supprimer delete_client.php (POST JSON) Bouton suppression → ClientService.deleteClient()
  4. Configuration du projet

    1. Étape 1 : Créer la base de données MySQL
      • Créer la base de données :
      • CREATE DATABASE base_api_php;
      • Créer la table utulisateurs:
      • CREATE TABLE utilisateurs (
          id int NOT NULL AUTO_INCREMENT,
          email varchar(180) COLLATE utf8mb4_unicode_ci NOT NULL,
          roles json NOT NULL,
          password varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
          PRIMARY KEY (id),
          UNIQUE KEY UNIQ_IDENTIFIER_EMAIL (email)
        ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
      • Créer la table Ajouter des utulisateurs:
      • Fichier add_user.php:
      • Ajouter un utilisateur de test en exécutant le fichier add_user.php:
      • <?php
        require_once('dbconnect.php');
        
        $email = 'test@example.com';
        $password = password_hash('123456', PASSWORD_DEFAULT);
        $roles = json_encode(['ROLE_USER']);
        
        $stmt = $pdo->prepare("INSERT INTO utilisateurs (email, password, roles) 
        VALUES (?, ?, ?)");
        $stmt->execute([$email, $password, $roles]);
        
        echo "Utilisateur ajouté.";
        ?>
        
    2. Étape 2 : Créer les scripts PHP
        Fichier config.php:
        <?php
            define('DB_HOST', '127.0.0.1');     // l'hôte de la base de données MySQL
            define('DB_NAME', 'base_api_php');    // le nom de la base de données MySQL
            define('DB_USER', 'root');    // le nom d'utilisateur MySQL
            define('DB_PASSWORD', '');    // le mot de passe MySQLhriadh
            define('DB_CHARSET', 'utf8');    // le jeu de caractères pour la base de données MySQL
        ?>
        Fichier dbconnect.php:
        <?php
        require_once('config.php');
        // Créer une nouvelle connexion PDO
        try {
            $pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET, 
            DB_USER, DB_PASSWORD);
            
            // Activer le mode d'erreur PDO
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            // Activer le mode d'émulation PDO pour désactiver l'utilisation de 'prepared statements'
            $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        } catch(PDOException $e) {
            // En cas d'erreur de connexion à la base de données, afficher un message d'erreur et arrêter le script
            die("Erreur de connexion à la base de données: ".$e->getMessage());
        }
        ?>
        Fichier login.php:
        <?php
        // Cela permet à tous les domaines (origines) d’accéder à cette 
        // ressource via une requête HTTP (notamment via AJAX ou fetch() en
        //  JavaScript/Flutter).
        // Le * (joker) autorise tout le monde. En production, on remplace souvent par un domaine spécifique :
        header("Access-Control-Allow-Origin: *");
        // Cela autorise les requêtes à inclure certains en-têtes personnalisés (headers), ici Content-Type.
        header("Access-Control-Allow-Headers: Content-Type");
        // Cela indique que la réponse du serveur est en JSON.
        header("Content-Type: application/json");
        
        require_once('dbconnect.php');
        
        // Récupérer les données JSON envoyées par Flutter
        $data = json_decode(file_get_contents("php://input"));
        
        $email = $data->email ?? '';
        $password = $data->password ?? '';
        
        if (empty($email) || empty($password)) {
            echo json_encode(['success' => false, 'message' => 'Email ou mot de passe manquant.']);
            exit;
        }
        
        // Requête pour trouver l'utilisateur
        $stmt = $pdo->prepare("SELECT * FROM utilisateurs WHERE email = ?");
        $stmt->execute([$email]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($user && password_verify($password, $user['password'])) {
            echo json_encode(['success' => true, 'user' => [
                'id' => $user['id'],
                'email' => $user['email'],
                'roles' => json_decode($user['roles'])
            ]]);
        } else {
            echo json_encode(['success' => false, 'message' => 'Identifiants invalides']);
        }
        ?>
        Fichier index.php:
        <?php
        /**
         * CONFIGURATION DES ENTÊTES (HEADERS)
         * Indispensable pour transformer PHP en une API accessible et compréhensible.
         */
        
        // Autorise les requêtes provenant de n'importe quel domaine (CORS)
        header("Access-Control-Allow-Origin: *");
        
        // Autorise spécifiquement l'envoi du header "Content-Type" par le client
        header("Access-Control-Allow-Headers: Content-Type");
        
        // Indique au navigateur/client que la réponse sera au format JSON (et non HTML)
        header("Content-Type: application/json");
        
        /**
         * LOGIQUE DU ROUTEUR
         */
        
        // 1. On récupère l'URL demandée, on la nettoie et on retire les slashs inutiles
        // Exemple : "/mon_api_php/login/" devient "mon_api_php/login"
        $route = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
        
        // 2. Système de routage : on vérifie si l'URL correspond à une page précise
        if ($route === 'mon_api_php/login') {
            // Si oui, on délègue le travail au fichier login.php
            require __DIR__ . '/login.php';
            // On arrête l'exécution ici pour ne pas envoyer le message "API OK" par erreur
            exit;
        }
        
        /**
         * RÉPONSE PAR DÉFAUT
         * Si aucune route n'a matché au-dessus, on renvoie un statut de santé de l'API.
         */
        echo json_encode([
            'success' => true,
            'message' => 'API OK'
        ]);
        ?>
        Fichier logout.php:
        <?php
           // mon_api_php/logout.php
        
        header("Access-Control-Allow-Origin: *");
        header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
        header("Access-Control-Allow-Headers: Content-Type, Authorization");
        header("Content-Type: application/json; charset=UTF-8");
        
        // Gérer OPTIONS
        if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
            http_response_code(200);
            exit();
        }
        
        // Pour logout, on accepte simplement la requête
        // En production, vous pourriez blacklister le token
        echo json_encode([
            'success' => true,
            'message' => 'Déconnexion réussie'
        ]);
        ?>
        Fichier register.php:
        <?php
        
        
        // Headers CORS et JSON - DOIVENT être en premier
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
        header('Access-Control-Allow-Headers: Content-Type, Accept, Authorization');
        header('Content-Type: application/json; charset=UTF-8');
        
        // Désactiver l'affichage des erreurs HTML
        ini_set('display_errors', 0);
        error_reporting(0);
        
        // Inclure la connexion à la base de données
        require_once('dbconnect.php');
        
        // Fonction pour retourner une réponse JSON
        function jsonResponse($data, $code = 200) {
            http_response_code($code);
            echo json_encode($data);
            exit;
        }
        
        // Gérer les requêtes OPTIONS (preflight CORS)
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            jsonResponse(['status' => 'OK'], 200);
        }
        
        // Test GET pour vérifier que le fichier fonctionne
        if ($_SERVER['REQUEST_METHOD'] === 'GET') {
            jsonResponse([
                'status' => 'OK',
                'message' => 'API register accessible'
            ]);
        }
        
        // Traitement POST pour l'inscription
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            try {
                // Lire les données JSON
                $json = file_get_contents('php://input');
                $data = json_decode($json, true);
                
                // Vérifier que le JSON est valide
                if (json_last_error() !== JSON_ERROR_NONE) {
                    jsonResponse([
                        'success' => false,
                        'message' => 'JSON invalide'
                    ], 400);
                }
                
                // Vérifier les champs requis
                if (empty($data['email']) || empty($data['password'])) {
                    jsonResponse([
                        'success' => false,
                        'message' => 'Email et mot de passe requis'
                    ], 400);
                }
                
                $email = trim($data['email']);
                $password = trim($data['password']);
                $nomComplet = isset($data['nom_complet']) ? trim($data['nom_complet']) : null;
                $pays = isset($data['pays']) ? trim($data['pays']) : null;
                $numTel = isset($data['num_tel']) ? trim($data['num_tel']) : null;
                $urlPhotoProfil = isset($data['url_photo_profil']) ? trim($data['url_photo_profil']) : null;
                
                // Gérer les rôles (par défaut ROLE_USER)
                $roles = isset($data['roles']) && is_array($data['roles']) 
                    ? $data['roles'] 
                    : ['ROLE_USER'];
                
                // Validation de l'email
                if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                    jsonResponse([
                        'success' => false,
                        'message' => 'Email invalide'
                    ], 400);
                }
                
                // Validation du mot de passe
                if (strlen($password) < 6) {
                    jsonResponse([
                        'success' => false,
                        'message' => 'Le mot de passe doit contenir au moins 6 caractères'
                    ], 400);
                }
                
                // Validation des rôles
                $rolesValides = ['ROLE_ADMIN', 'ROLE_USER', 'ROLE_CLIENT', 'ROLE_VENDEUR'];
                foreach ($roles as $role) {
                    if (!in_array($role, $rolesValides)) {
                        jsonResponse([
                            'success' => false,
                            'message' => 'Rôle invalide: ' . $role
                        ], 400);
                    }
                }
                
                // Vérifier si l'email existe déjà
                $stmt = $pdo->prepare("SELECT id FROM utilisateurs WHERE email = ?");
                $stmt->execute([$email]);
                
                if ($stmt->rowCount() > 0) {
                    jsonResponse([
                        'success' => false,
                        'message' => 'Cet email est déjà utilisé'
                    ], 409);
                }
                
                // Hasher le mot de passe
                $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
                
                // Créer le JSON pour les rôles
                $rolesJson = json_encode($roles);
                
                // Insérer le nouvel utilisateur avec les bons noms de colonnes
                $stmt = $pdo->prepare(
                    "INSERT INTO utilisateurs (email, mot_de_passe_hache, nom_complet, pays, num_tel, url_photo_profil, roles) 
                     VALUES (?, ?, ?, ?, ?, ?, ?)"
                );
                $stmt->execute([$email, $hashedPassword, $nomComplet, $pays, $numTel, $urlPhotoProfil, $rolesJson]);
                
                // Récupérer l'ID du nouvel utilisateur
                $userId = $pdo->lastInsertId();
                
                // Générer un token (optionnel)
                $token = bin2hex(random_bytes(32));
                
                // Réponse de succès
                jsonResponse([
                    'success' => true,
                    'message' => 'Inscription réussie',
                    'token' => $token,
                    'user' => [
                        'id' => (int)$userId,
                        'email' => $email,
                        'nom_complet' => $nomComplet,
                        'pays' => $pays,
                        'num_tel' => $numTel,
                        'url_photo_profil' => $urlPhotoProfil,
                        'roles' => $roles
                    ]
                ], 201);
                
            } catch (PDOException $e) {
                jsonResponse([
                    'success' => false,
                    'message' => 'Erreur de base de données: ' . $e->getMessage()
                ], 500);
            } catch (Exception $e) {
                jsonResponse([
                    'success' => false,
                    'message' => 'Erreur serveur: ' . $e->getMessage()
                ], 500);
            }
        }
        
        // Méthode non autorisée
        jsonResponse([
            'success' => false,
            'message' => 'Méthode non autorisée'
        ], 405);
        ?>
    3. Étape 3 : Créer le projet Flutter
      • Créer le projet :
      • flutter create exemple01_php
      • Ajouter la dépendance HTTP dans pubspec.yaml:
      • dependencies:
          flutter:
            sdk: flutter
          http: ^0.13.5
    4. Architecture du projet (Dart / Flutter)
        lib/ │ ├── controllers/ │ ├── auth_controller.dart │ └── product_controller.dart │ ├── models/ │ ├── product_model.dart │ └── user_model.dart │ ├── services/ │ ├── api_base.dart │ ├── auth_api.dart │ └── product_api.dart │ ├── views/ │ ├── admin_dashboard.dart │ ├── client_dashboard.dart │ ├── vendeur_dashboard.dart │ ├── dashboard_router.dart │ ├── home_page.dart │ ├── login_page.dart │ ├── register_page.dart │ └── products_page.dart │ └── main.dart
    5. Étape 4 : Les fichiers Controllers/
        Fichier : main.dart
        import 'package:exemple01/controllers/product_controller.dart';
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import 'controllers/auth_controller.dart';
        import 'views/login_page.dart';
        
        void main() {
          runApp(const MyApp());
        }
        
        class MyApp extends StatelessWidget {
          const MyApp({super.key});
        
          @override
          Widget build(BuildContext context) {
            return MultiProvider(
          providers: [
            ChangeNotifierProvider(create: (_) => AuthController()),
          ],
              child: MaterialApp(
                debugShowCheckedModeBanner: false,
                home: LoginPage(),
              ),
            );
          }
        }
        
      • Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget MainApp qui affiche directement la page de connexion LoginPage.
    6. Dossier 'Controllers'
        Fichier : auth_controller.dart
        import 'package:exemple01/services/api_base.dart';
        import 'package:flutter/material.dart';
        import 'package:shared_preferences/shared_preferences.dart';
        import '../services/auth_api.dart';
        import '../models/user_model.dart';
        
        class AuthController with ChangeNotifier {
          User? _user;
          String? _token;
          bool _isLoading = false;
          String? _errorMessage;
        
          User? get user => _user;
          String? get token => _token;
          bool get isLoading => _isLoading;
          String? get errorMessage => _errorMessage;
          bool get isAuthenticated => _user != null && _token != null;
        
          // Méthode de connexion
          Future<bool> login(String email, String password) async {
            _isLoading = true;
            _errorMessage = null;
            notifyListeners();
        
            try {
              final result = await AuthApi.login(email, password);
              
              if (result['success'] == true) {
                _user = User.fromJson(result['user']);
                _token = result['token'] ?? '';
                _errorMessage = null;
                
                // Sauvegarder le token et les données utilisateur
                await _saveToken(_token!);
                await _saveUser(_user!);
                
                _isLoading = false;
                notifyListeners();
                return true;
              } else {
                _errorMessage = result['message'];
                _isLoading = false;
                notifyListeners();
                return false;
              }
            } catch (e) {
              _errorMessage = 'Erreur de connexion: $e';
              _isLoading = false;
              notifyListeners();
              return false;
            }
          }
        
          // Méthode d'inscription MODIFIÉE
          Future<bool> register({
          required String email,
          required String password,
          String? nomComplet,
          String? pays,
          String? numTel,
          String? urlPhotoProfil,
          List<String>? roles,
        }) async {
          _isLoading = true;
          _errorMessage = null;
          notifyListeners();
        
          try {
            // ✅ Forcer le rôle sélectionné ou ROLE_USER par défaut
            final List<String> finalRoles =
                (roles != null && roles.isNotEmpty)
                    ? roles
                    : ['ROLE_USER'];
        
            final data = await AuthApi.register(
              email: email,
              password: password,
              nomComplet: nomComplet,
              pays: pays,
              numTel: numTel,
              urlPhotoProfil: urlPhotoProfil,
              roles: finalRoles, // ✅ rôle garanti
            );
        
            if (data['success'] == true) {
              _user = User.fromJson(data['user']);
              _token = data['token'];
        
              await _saveToken(_token!);
              await _saveUser(_user!);
        
              _isLoading = false;
              notifyListeners();
              return true;
            } else {
              _errorMessage = data['message'] ?? "Erreur d'inscription";
              _isLoading = false;
              notifyListeners();
              return false;
            }
          } catch (e) {
            _errorMessage = e.toString();
            _isLoading = false;
            notifyListeners();
            return false;
          }
        }
        
        
          // Méthode de déconnexion
          Future<void> logout() async {
            _isLoading = true;
            notifyListeners();
        
            try {
              if (_token != null && _token!.isNotEmpty) {
                try {
                  await AuthApi.logout(_token!);
                  print('✅ Déconnexion API réussie');
                } catch (e) {
                  print('⚠️ Erreur lors de la déconnexion API (ignorée): $e');
                }
              }
              
              await _clearLocalData();
              
            } catch (e) {
              print('⚠️ Erreur dans logout (nettoyage local): $e');
              await _clearLocalData();
            } finally {
              _isLoading = false;
              notifyListeners();
            }
          }
        
          // Sauvegarder le token dans SharedPreferences
          Future<void> _saveToken(String token) async {
            try {
              final prefs = await SharedPreferences.getInstance();
              await prefs.setString('auth_token', token);
              print('✅ Token sauvegardé');
            } catch (e) {
              print('⚠️ Erreur lors de la sauvegarde du token: $e');
            }
          }
        
          // Sauvegarder les données utilisateur (NOUVEAU)
          Future<void> _saveUser(User user) async {
            try {
              final prefs = await SharedPreferences.getInstance();
              await prefs.setString('user_email', user.email);
              await prefs.setString('user_roles', user.roles.join(','));
              if (user.nomComplet != null) {
                await prefs.setString('user_nom_complet', user.nomComplet!);
              }
              if (user.id != null) {
                await prefs.setInt('user_id', user.id!);
              }
              print('✅ Données utilisateur sauvegardées');
            } catch (e) {
              print('⚠️ Erreur lors de la sauvegarde des données utilisateur: $e');
            }
          }
        
          // Charger le token et l'utilisateur depuis SharedPreferences (MODIFIÉ)
          Future<void> loadToken() async {
            try {
              final prefs = await SharedPreferences.getInstance();
              _token = prefs.getString('auth_token');
              
              if (_token != null) {
                // Charger aussi les données utilisateur
                final userId = prefs.getInt('user_id');
                final userEmail = prefs.getString('user_email');
                final userRolesString = prefs.getString('user_roles');
                final userNomComplet = prefs.getString('user_nom_complet');
                
                if (userEmail != null && userRolesString != null) {
                  _user = User(
                    id: userId,
                    email: userEmail,
                    nomComplet: userNomComplet,
                    roles: userRolesString.split(','),
                  );
                  print('✅ Token et utilisateur chargés depuis le stockage');
                } else {
                  print('✅ Token chargé depuis le stockage');
                }
                
                notifyListeners();
              }
            } catch (e) {
              print('⚠️ Erreur lors du chargement du token: $e');
            }
          }
        
          // Nettoyer les données locales (MODIFIÉ)
          Future<void> _clearLocalData() async {
            _user = null;
            _token = null;
            _errorMessage = null;
            
            try {
              final prefs = await SharedPreferences.getInstance();
              await prefs.remove('auth_token');
              await prefs.remove('user_id');
              await prefs.remove('user_email');
              await prefs.remove('user_roles');
              await prefs.remove('user_nom_complet');
              print('✅ Données locales nettoyées');
            } catch (e) {
              print('⚠️ Erreur lors du nettoyage des données: $e');
            }
          }
        
          // Forcer la déconnexion sans appeler l'API
          Future<void> forceLogout() async {
            await _clearLocalData();
            notifyListeners();
          }
        
          // Effacer le message d'erreur
          void clearError() {
            _errorMessage = null;
            notifyListeners();
          }
        
          // Méthode pour simuler une connexion (pour tests)
          void mockLogin() {
            _user = User(
              id: 1,
              email: 'test@test.com',
              nomComplet: 'Test User',
              roles: ['ROLE_USER'],
            );
            _token = 'mock_token_123456';
            notifyListeners();
          }
        
          // Vérifier si l'utilisateur a un rôle spécifique (NOUVEAU)
          bool hasRole(String role) {
            return _user?.hasRole(role) ?? false;
          }
        
          // Getters pour les rôles (NOUVEAU)
          bool get isAdmin => _user?.isAdmin ?? false;
          bool get isVendeur => _user?.isVendeur ?? false;
          bool get isClient => _user?.isClient ?? false;
        }
        Fichier : product_controller.dart
        import 'package:flutter/material.dart';
        import '../models/product_model.dart';
        import '../services/product_api.dart';
        
        class ProductController extends ChangeNotifier {
          final int idUtilisateur;
        
          ProductController(this.idUtilisateur);
        
          List<Product> _products = [];
          bool _isLoading = false;
          String? _errorMessage;
          Product? _selectedProduct;
        
          List<Product> get products => _products;
          bool get isLoading => _isLoading;
          String? get errorMessage => _errorMessage;
          Product? get selectedProduct => _selectedProduct;
        
          // Charger tous les produits
         Future<void> loadProductsByUtilisateur(int idUtilisateur) async {
          _isLoading = true;
          notifyListeners();
        
          try {
            _products = await ProductApi.getProductsByUtilisateur(idUtilisateur);
            _isLoading = false;
            notifyListeners();
          } catch (e) {
            _errorMessage = e.toString();
            _isLoading = false;
            notifyListeners();
          }
        }
        
        
          // Charger un produit par ID
          Future<void> loadProductById(int id) async {
            _isLoading = true;
            _errorMessage = null;
            notifyListeners();
        
            try {
              _selectedProduct = await ProductApi.getProductById(id);
              _isLoading = false;
              notifyListeners();
            } catch (e) {
              _errorMessage = e.toString();
              _isLoading = false;
              notifyListeners();
            }
          }
        
          // Ajouter un produit
          Future<bool> addProduct(Product product) async {
            _isLoading = true;
            _errorMessage = null;
            notifyListeners();
        
            try {
              await ProductApi.addProduct(product);
              await loadProductsByUtilisateur(idUtilisateur);  // Recharger la liste
              _isLoading = false;
              notifyListeners();
              return true;
            } catch (e) {
              _errorMessage = e.toString();
              _isLoading = false;
              notifyListeners();
              return false;
            }
          }
        
          // Modifier un produit
          Future<bool> updateProduct(Product product) async {
            _isLoading = true;
            _errorMessage = null;
            notifyListeners();
        
            try {
              await ProductApi.updateProduct(product);
              await loadProductsByUtilisateur(idUtilisateur); // ✅
              _isLoading = false;
              notifyListeners();
              return true;
            } catch (e) {
              _errorMessage = e.toString();
              _isLoading = false;
              notifyListeners();
              return false;
            }
          }
        
          // Supprimer un produit
          Future<bool> deleteProduct(int id) async {
            _isLoading = true;
            _errorMessage = null;
            notifyListeners();
        
            try {
              await ProductApi.deleteProduct(id);
              await loadProductsByUtilisateur(idUtilisateur); // ✅
        
              _isLoading = false;
              notifyListeners();
              return true;
            } catch (e) {
              _errorMessage = e.toString();
              _isLoading = false;
              notifyListeners();
              return false;
            }
          }
        
          // Rechercher des produits
          List<Product> searchProducts(String query) {
            if (query.isEmpty) return _products;
            
            return _products.where((product) {
              return product.nom.toLowerCase().contains(query.toLowerCase()) ||
                     product.description.toLowerCase().contains(query.toLowerCase());
            }).toList();
          }
        
          // Filtrer par prix
          List<Product> filterByPrice(double minPrice, double maxPrice) {
            return _products.where((product) {
              return product.prix >= minPrice && product.prix <= maxPrice;
            }).toList();
          }
        
          // Trier les produits
          void sortProducts(String sortBy) {
            switch (sortBy) {
              case 'nom_asc':
                _products.sort((a, b) => a.nom.compareTo(b.nom));
                break;
              case 'nom_desc':
                _products.sort((a, b) => b.nom.compareTo(a.nom));
                break;
              case 'prix_asc':
                _products.sort((a, b) => a.prix.compareTo(b.prix));
                break;
              case 'prix_desc':
                _products.sort((a, b) => b.prix.compareTo(a.prix));
                break;
              case 'date_asc':
                _products.sort((a, b) => (a.createdAt ?? DateTime.now())
                    .compareTo(b.createdAt ?? DateTime.now()));
                break;
              case 'date_desc':
                _products.sort((a, b) => (b.createdAt ?? DateTime.now())
                    .compareTo(a.createdAt ?? DateTime.now()));
                break;
            }
            notifyListeners();
          }
        }
    7. Dossier 'Models'
        Fichier : product_model.dart
        class Product {
          final int? id;
          final String nom;
          final String description;
          final double prix;
          final int quantite;
          final String? imageUrl;
          int idUtilisateur; // vendeur
          final DateTime? createdAt;
          final DateTime? updatedAt;
        
          Product({
            this.id,
            required this.nom,
            required this.description,
            required this.prix,
            this.quantite = 0,
            this.imageUrl,
            required this.idUtilisateur,
            this.createdAt,
            this.updatedAt,
          });
        
          // Convertir JSON en objet Product
          factory Product.fromJson(Map<String, dynamic> json) {
            return Product(
              id: json['id'] != null ? int.parse(json['id'].toString()) : null,
              nom: json['nom'] ?? '',
              description: json['description'] ?? '',
              prix: json['prix'] != null ? double.parse(json['prix'].toString()) : 0.0,
              quantite: json['quantite'] != null ? int.parse(json['quantite'].toString()) : 0,
              imageUrl: json['image_url'],
              idUtilisateur: json['id_utilisateur'] ?? json['idUtilisateur'] ?? 0,
              createdAt: json['created_at'] != null 
                  ? DateTime.parse(json['created_at'])
                  : null,
              updatedAt: json['updated_at'] != null 
                  ? DateTime.parse(json['updated_at'])
                  : null,
            );
          }
        
          // Convertir objet Product en JSON
          Map<String, dynamic> toJson() {
            return {
              'id': id,
              'nom': nom,
              'description': description,
              'prix': prix,
              'quantite': quantite,
              'image_url': imageUrl,
              'id_utilisateur': idUtilisateur, // correspond à la colonne MySQL
              'created_at': createdAt?.toIso8601String(),
              'updated_at': updatedAt?.toIso8601String(),
            };
          }
        
          // Copier avec modification
          Product copyWith({
            int? id,
            String? nom,
            String? description,
            double? prix,
            int? quantite,
            String? imageUrl,
            int? idUtilisateur,
            DateTime? createdAt,
            DateTime? updatedAt,
          }) {
            return Product(
              id: id ?? this.id,
              nom: nom ?? this.nom,
              description: description ?? this.description,
              prix: prix ?? this.prix,
              quantite: quantite ?? this.quantite,
              imageUrl: imageUrl ?? this.imageUrl,
              idUtilisateur: idUtilisateur ?? this.idUtilisateur,
              createdAt: createdAt ?? this.createdAt,
              updatedAt: updatedAt ?? this.updatedAt,
            );
          }
        }
        
        Fichier : user_model.dart
        // models/user_model.dart
        import 'dart:convert';
        
        class User {
          final int? id;
          final String email;
          final String? nomComplet;
          final String? pays;
          final String? numTel;
          final String? urlPhotoProfil;
          final List<String> roles; // Liste des rôles
          final DateTime? creeA;
          final DateTime? misAJourA;
        
          User({
            this.id,
            required this.email,
            this.nomComplet,
            this.pays,
            this.numTel,
            this.urlPhotoProfil,
            required this.roles,
            this.creeA,
            this.misAJourA,
          });
        
          factory User.fromJson(Map<String, dynamic> json) {
            // Parser les rôles JSON
            List<String> rolesList = [];
            if (json['roles'] != null) {
              if (json['roles'] is String) {
                // Si c'est une string JSON
                try {
                  rolesList = List<String>.from(jsonDecode(json['roles']));
                } catch (e) {
                  rolesList = ['ROLE_USER'];
                }
              } else if (json['roles'] is List) {
                // Si c'est déjà une liste
                rolesList = List<String>.from(json['roles']);
              }
            }
        
            return User(
              id: json['id'] != null ? int.parse(json['id'].toString()) : null,
              email: json['email'] ?? '',
              nomComplet: json['nom_complet'],
              pays: json['pays'],
              numTel: json['num_tel'],
              urlPhotoProfil: json['url_photo_profil'],
              roles: rolesList.isEmpty ? ['ROLE_USER'] : rolesList,
              creeA: json['cree_a'] != null ? DateTime.parse(json['cree_a']) : null,
              misAJourA: json['mis_a_jour_a'] != null ? DateTime.parse(json['mis_a_jour_a']) : null,
            );
          }
        
          Map<String, dynamic> toJson() {
            return {
              'id': id,
              'email': email,
              'nom_complet': nomComplet,
              'pays': pays,
              'num_tel': numTel,
              'url_photo_profil': urlPhotoProfil,
              'roles': roles,
            };
          }
        
          // Méthodes pour vérifier les rôles
          bool hasRole(String role) => roles.contains(role);
          
          bool get isAdmin => hasRole('ROLE_ADMIN');
          bool get isVendeur => hasRole('ROLE_VENDEUR');
          bool get isClient => hasRole('ROLE_USER') || hasRole('ROLE_CLIENT');
          
          // Obtenir le rôle principal (priorité)
          String get mainRole {
            if (isAdmin) return 'ROLE_ADMIN';
            if (isVendeur) return 'ROLE_VENDEUR';
            return 'ROLE_USER';
          }
        }
    8. Dossier 'Services'
        Fichier : api_base.dart
        import 'dart:async';
        import 'dart:convert';
        import 'dart:io';
        
        import 'package:http/http.dart' as http;
        
        class ApiBase {
          // URL de base corrigée
          static String baseUrl = 'http://10.0.2.2/flutter/mon_api_php';
          
          // Méthode principale pour toutes les requêtes
          static Future<Map<String, dynamic>> request({
            required String method,
            required String endpoint,
            Map<String, dynamic>? body,
            String? token,
          }) async {
            try {
              // Nettoyer l'endpoint (enlever les slashes en trop)
              String cleanEndpoint = endpoint.trim();
              if (cleanEndpoint.startsWith('/')) {
                cleanEndpoint = cleanEndpoint.substring(1);
              }
              
              // Ajouter .php si pas présent et pas d'autre extension
              if (!cleanEndpoint.contains('.')) {
                cleanEndpoint = '$cleanEndpoint.php';
                print('? Extension ajoutée: $cleanEndpoint');
              }
              
              // Construire l'URL
              final url = Uri.parse('$baseUrl/$cleanEndpoint');
              print('? Requête API: $method $url');
              
              // Headers
              final headers = {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                if (token != null) 'Authorization': 'Bearer $token',
              };
              
              // Log du body pour debug
              if (body != null) {
                print('? Body: ${json.encode(body)}');
              }
        
              // Faire la requête avec timeout
              http.Response response;
              
              switch (method.toUpperCase()) {
                case 'GET':
                  response = await http
                      .get(url, headers: headers)
                      .timeout(const Duration(seconds: 15));
                  break;
                case 'POST':
                  response = await http
                      .post(
                        url,
                        headers: headers,
                        body: body != null ? json.encode(body) : null,
                      )
                      .timeout(const Duration(seconds: 15));
                  break;
                case 'PUT':
                  response = await http
                      .put(
                        url,
                        headers: headers,
                        body: body != null ? json.encode(body) : null,
                      )
                      .timeout(const Duration(seconds: 15));
                  break;
                case 'DELETE':
                  response = await http
                      .delete(url, headers: headers)
                      .timeout(const Duration(seconds: 15));
                  break;
                default:
                  throw Exception('Méthode non supportée: $method');
              }
        
              print('? Réponse: ${response.statusCode}');
              print('? Body: ${response.body}');
              
              // Parser la réponse
              return _parseResponse(response);
              
            } on SocketException {
              throw Exception('Pas de connexion internet. Vérifiez votre réseau.');
            } on TimeoutException {
              throw Exception('Le serveur ne répond pas. Vérifiez que XAMPP est démarré.');
            } on http.ClientException catch (e) {
              throw Exception('Erreur réseau: ${e.message}');
            } on FormatException catch (e) {
              throw Exception('Format JSON invalide: $e');
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
          
          // Parser la réponse
          static Map<String, dynamic> _parseResponse(http.Response response) {
            // Vérifier si c'est du HTML (erreur commune)
            if (response.body.trim().startsWith('<!DOCTYPE') || 
                response.body.trim().startsWith('<html') ||
                response.body.trim().startsWith('<?xml')) {
              throw Exception(
                'Le serveur retourne du HTML au lieu de JSON. '
                'Vérifiez l\'URL et que le fichier PHP existe.'
              );
            }
            
            // Vérifier si la réponse est vide
            if (response.body.isEmpty) {
              throw Exception('Réponse vide du serveur');
            }
            
            try {
              final data = json.decode(response.body);
              
              // Gérer les différents codes de statut
              if (response.statusCode >= 200 && response.statusCode < 300) {
                return data;
              } else if (response.statusCode == 400) {
                throw Exception(data['message'] ?? 'Requête invalide');
              } else if (response.statusCode == 401) {
                throw Exception(data['message'] ?? 'Non autorisé');
              } else if (response.statusCode == 404) {
                throw Exception('API introuvable. Vérifiez l\'URL: $baseUrl');
              } else if (response.statusCode == 406) {
                throw Exception(
                  'Erreur 406: Le serveur n\'accepte pas le format de la requête. '
                  'Vérifiez les headers dans login.php'
                );
              } else if (response.statusCode == 500) {
                throw Exception(data['message'] ?? 'Erreur serveur');
              } else {
                throw Exception(data['message'] ?? 'Erreur ${response.statusCode}');
              }
            } on FormatException {
              // Si le JSON est invalide, afficher un extrait du corps
              final preview = response.body.length > 200 
                  ? '${response.body.substring(0, 200)}...' 
                  : response.body;
              throw Exception('Réponse invalide (JSON attendu): $preview');
            }
          }
          
          // Tester la connexion à l'API
          static Future<bool> testConnection() async {
            print('? Test de connexion à $baseUrl');
            
            try {
              // Créer un endpoint de test
              final testUrl = Uri.parse('$baseUrl/test.php');
              print('? Test: GET $testUrl');
              
              final response = await http
                  .get(testUrl)
                  .timeout(const Duration(seconds: 10));
              
              print('? Réponse test: ${response.statusCode}');
              print('? Body: ${response.body}');
              
              if (response.statusCode == 200) {
                print('✅ Serveur accessible');
                
                // Vérifier le contenu
                if (response.body.contains('success') ||
                    response.body.contains('status') ||
                    response.body.startsWith('{') || 
                    response.body.startsWith('[')) {
                  print('✅ API semble fonctionner correctement');
                  return true;
                } else {
                  print('⚠️ Contenu inattendu');
                  return false;
                }
              } else if (response.statusCode == 404) {
                print('❌ Fichier test.php introuvable');
                print('? Créez un fichier test.php dans $baseUrl');
                return false;
              } else {
                print('❌ Erreur HTTP: ${response.statusCode}');
                return false;
              }
            } on SocketException {
              print('❌ Impossible de se connecter au serveur');
              print('? Vérifications:');
              print('   1. XAMPP est démarré?');
              print('   2. Apache est en cours d\'exécution?');
              print('   3. Le dossier existe: C:\\xampp\\htdocs\\flutter\\mon_api_php\\');
              return false;
            } on TimeoutException {
              print('❌ Timeout - Le serveur ne répond pas');
              return false;
            } catch (e) {
              print('❌ Erreur: $e');
              return false;
            }
          }
          
          // Méthodes pratiques pour chaque type de requête
          static Future<Map<String, dynamic>> get(
            String endpoint, {
            String? token,
          }) async {
            return request(
              method: 'GET',
              endpoint: endpoint,
              token: token,
            );
          }
          
          static Future<Map<String, dynamic>> post(
            String endpoint, {
            Map<String, dynamic>? body,
            String? token,
          }) async {
            return request(
              method: 'POST',
              endpoint: endpoint,
              body: body,
              token: token,
            );
          }
          
          static Future<Map<String, dynamic>> put(
            String endpoint, {
            Map<String, dynamic>? body,
            String? token,
          }) async {
            return request(
              method: 'PUT',
              endpoint: endpoint,
              body: body,
              token: token,
            );
          }
          
          static Future<Map<String, dynamic>> delete(
            String endpoint, {
            String? token,
          }) async {
            return request(
              method: 'DELETE',
              endpoint: endpoint,
              token: token,
            );
          }
        }
        Fichier : auth_api.dart
        import 'api_base.dart';
        
        class AuthApi {
          // Login
          static Future<Map<String, dynamic>> login(
              String email, String password) async {
            try {
              final response = await ApiBase.request(
                method: 'POST',
                endpoint: 'login',
                body: {
                  'email': email,
                  'password': password,
                },
              );
        
              return response;
            } catch (e) {
              return {
                'success': false,
                'message': 'Erreur de connexion: $e',
              };
            }
          }
        
          // Register avec tous les champs de ta table
          static Future<Map<String, dynamic>> register({
            required String email,
            required String password,
            String? nomComplet,
            String? pays,
            String? numTel,
            String? urlPhotoProfil,
            List<String>? roles,
          }) async {
            try {
              final response = await ApiBase.request(
                method: 'POST',
                endpoint: 'register',
                body: {
                  'email': email,
                  'password': password,
                  if (nomComplet != null) 'nom_complet': nomComplet,
                  if (pays != null) 'pays': pays,
                  if (numTel != null) 'num_tel': numTel,
                  if (urlPhotoProfil != null) 'url_photo_profil': urlPhotoProfil,
                  'roles': roles ?? ['ROLE_USER'], // Par défaut ROLE_USER
                },
              );
        
              return response;
            } catch (e) {
              return {
                'success': false,
                'message': 'Erreur d\'inscription: $e',
              };
            }
          }
        
          // Logout
          static Future<Map<String, dynamic>> logout(String token) async {
            try {
              final response = await ApiBase.request(
                method: 'POST',
                endpoint: 'logout',
                token: token,
              );
        
              return response;
            } catch (e) {
              // Pour logout, on ne throw pas - on retourne un succès simulé
              if (e.toString().contains('404') || 
                  e.toString().contains('Endpoint non trouvé')) {
                return {
                  'success': true,
                  'message': 'Déconnexion locale (endpoint non disponible)',
                };
              }
              
              // Autres erreurs
              return {
                'success': true, // On considère quand même que c'est un succès
                'message': 'Déconnexion effectuée (erreur ignorée: $e)',
              };
            }
          }
        
          // Méthode de test pour vérifier quels endpoints sont disponibles
          static Future<void> testEndpoints() async {
            print('? Test des endpoints d\'authentification');
            
            final endpoints = ['login', 'logout', 'register'];
            
            for (var endpoint in endpoints) {
              print('\nTest endpoint: $endpoint');
              try {
                final response = await ApiBase.request(
                  method: endpoint == 'login' || endpoint == 'register' ? 'POST' : 'GET',
                  endpoint: endpoint,
                  body: endpoint == 'login' || endpoint == 'register' 
                      ? {'test': 'data'} 
                      : null,
                );
                print('  ✅ Disponible: ${response['success']}');
              } catch (e) {
                print('  ❌ Non disponible: $e');
              }
            }
          }
        }
        Fichier : product_api.dart
        import 'dart:convert';
        import 'package:http/http.dart' as http;
        import '../models/product_model.dart';
        import 'api_base.dart';
        
        class ProductApi {
          // Récupérer tous les produits
          static Future<List<Product>> getAllProducts() async {
            try {
              final data = await ApiBase.get('produits/get_products.php');
        
              if (data['success'] == true) {
                List<Product> products = (data['produits'] as List)
                    .map((product) => Product.fromJson(product))
                    .toList();
                return products;
              } else {
                throw Exception(data['error'] ?? 'Erreur lors de la récupération des produits');
              }
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
        
          // Récupérer un produit par ID
          static Future<Product> getProductById(int id) async {
            try {
              final data = await ApiBase.get('produits/get_product.php?id=$id');
        
              if (data['success'] == true) {
                return Product.fromJson(data['produit']);
              } else {
                throw Exception(data['error'] ?? 'Produit non trouvé');
              }
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
        
          // Ajouter un produit
          static Future<Map<String, dynamic>> addProduct(Product product) async {
            try {
              final data = await ApiBase.post(
                'produits/add_product.php',
                body: product.toJson(),
              );
        
              if (data['success'] == true) {
                return data;
              } else {
                throw Exception(data['error'] ?? 'Erreur lors de l\'ajout du produit');
              }
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
        
          // Modifier un produit
          static Future<Map<String, dynamic>> updateProduct(Product product) async {
            try {
              final data = await ApiBase.put(
                'produits/update_product.php',
                body: product.toJson(),
              );
        
              if (data['success'] == true) {
                return data;
              } else {
                throw Exception(data['error'] ?? 'Erreur lors de la modification du produit');
              }
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
        
          // Supprimer un produit
          static Future<Map<String, dynamic>> deleteProduct(int id) async {
            try {
              final data = await ApiBase.delete('produits/delete_product.php?id=$id');
        
              if (data['success'] == true) {
                return data;
              } else {
                throw Exception(data['error'] ?? 'Erreur lors de la suppression du produit');
              }
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
        
          // Récupérer les produits d'un utilisateur
          static Future<List<Product>> getProductsByUtilisateur(int idUtilisateur) async {
            try {
              final data = await ApiBase.get(
                'produits/get_products.php?id_utilisateur=$idUtilisateur',
              );
        
              if (data['success'] == true) {
                return (data['produits'] as List)
                    .map((p) => Product.fromJson(p))
                    .toList();
              } else {
                throw Exception(data['error'] ?? 'Erreur lors de la récupération des produits');
              }
            } catch (e) {
              throw Exception('Erreur: $e');
            }
          }
        }
        
    9. Dossier 'Views'
        Fichier : login_page.dart
        Fichier : admin_dashboard.dart
        import 'package:flutter/material.dart';
        import '../models/user_model.dart';
        import 'package:provider/provider.dart';
        import '../controllers/auth_controller.dart';
        import 'login_page.dart';
        
        class AdminDashboard extends StatelessWidget {
          final User user;
        
          const AdminDashboard({super.key, required this.user});
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: const Text('Dashboard Admin'),
                backgroundColor: Colors.red,
                foregroundColor: Colors.white,
                actions: [
                  IconButton(
                    icon: const Icon(Icons.logout),
                    onPressed: () async {
                      await context.read<AuthController>().logout();
                      if (context.mounted) {
                        Navigator.pushAndRemoveUntil(
                          context,
                          MaterialPageRoute(builder: (_) => const LoginPage()),
                          (route) => false,
                        );
                      }
                    },
                  ),
                ],
              ),
              body: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // En-tête
                    Container(
                      padding: const EdgeInsets.all(20),
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: [Colors.red.shade700, Colors.red.shade500],
                        ),
                        borderRadius: BorderRadius.circular(16),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.red.withOpacity(0.3),
                            blurRadius: 10,
                            offset: const Offset(0, 4),
                          ),
                        ],
                      ),
                      child: Row(
                        children: [
                          const CircleAvatar(
                            radius: 30,
                            backgroundColor: Colors.white,
                            child: Icon(Icons.admin_panel_settings, size: 35, color: Colors.red),
                          ),
                          const SizedBox(width: 16),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  user.nomComplet ?? user.email,
                                  style: const TextStyle(
                                    fontSize: 20,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white,
                                  ),
                                ),
                                const SizedBox(height: 4),
                                Text(
                                  'Administrateur',
                                  style: TextStyle(
                                    fontSize: 14,
                                    color: Colors.white.withOpacity(0.9),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
        
                    const SizedBox(height: 32),
        
                    // Grille de cartes
                    Expanded(
                      child: GridView.count(
                        crossAxisCount: 2,
                        crossAxisSpacing: 16,
                        mainAxisSpacing: 16,
                        children: [
                          _DashboardCard(
                            title: 'Gestion\nUtilisateurs',
                            icon: Icons.people,
                            color: Colors.blue,
                            onTap: () {
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Gestion Utilisateurs - À venir')),
                              );
                            },
                          ),
                          _DashboardCard(
                            title: 'Gestion\nProduits',
                            icon: Icons.inventory,
                            color: Colors.green,
                            onTap: () {
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Gestion Produits - À venir')),
                              );
                            },
                          ),
                          _DashboardCard(
                            title: 'Statistiques',
                            icon: Icons.bar_chart,
                            color: Colors.orange,
                            onTap: () {
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Statistiques - À venir')),
                              );
                            },
                          ),
                          _DashboardCard(
                            title: 'Validation\nInscriptions',
                            icon: Icons.check_circle,
                            color: Colors.purple,
                            onTap: () {
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Validation - À venir')),
                              );
                            },
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            );
          }
        }
        
        class _DashboardCard extends StatelessWidget {
          final String title;
          final IconData icon;
          final Color color;
          final VoidCallback onTap;
        
          const _DashboardCard({
            required this.title,
            required this.icon,
            required this.color,
            required this.onTap,
          });
        
          @override
          Widget build(BuildContext context) {
            return Card(
              elevation: 4,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              child: InkWell(
                onTap: onTap,
                borderRadius: BorderRadius.circular(16),
                child: Container(
                  padding: const EdgeInsets.all(16.0),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(16),
                    gradient: LinearGradient(
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                      colors: [
                        color.withOpacity(0.1),
                        color.withOpacity(0.05),
                      ],
                    ),
                  ),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(icon, size: 48, color: color),
                      const SizedBox(height: 12),
                      Text(
                        title,
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 14,
                          fontWeight: FontWeight.bold,
                          color: color,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            );
          }
        }
        Fichier : client_dashboard.dart
        import 'package:flutter/material.dart';
        import '../models/user_model.dart';
        import 'package:provider/provider.dart';
        import '../controllers/auth_controller.dart';
        import 'login_page.dart';
        
        class ClientDashboard extends StatelessWidget {
          final User user;
        
          const ClientDashboard({super.key, required this.user});
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: const Text('Catalogue Produits'),
                backgroundColor: Colors.blue,
                foregroundColor: Colors.white,
                actions: [
                  IconButton(
                    icon: const Icon(Icons.shopping_cart),
                    onPressed: () {
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Panier - À venir')),
                      );
                    },
                  ),
                  IconButton(
                    icon: const Icon(Icons.logout),
                    onPressed: () async {
                      await context.read<AuthController>().logout();
                      if (context.mounted) {
                        Navigator.pushAndRemoveUntil(
                          context,
                          MaterialPageRoute(builder: (_) => const LoginPage()),
                          (route) => false,
                        );
                      }
                    },
                  ),
                ],
              ),
              body: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // En-tête
                    Container(
                      padding: const EdgeInsets.all(20),
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: [Colors.blue.shade700, Colors.blue.shade500],
                        ),
                        borderRadius: BorderRadius.circular(16),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.blue.withOpacity(0.3),
                            blurRadius: 10,
                            offset: const Offset(0, 4),
                          ),
                        ],
                      ),
                      child: Row(
                        children: [
                          const CircleAvatar(
                            radius: 30,
                            backgroundColor: Colors.white,
                            child: Icon(Icons.shopping_bag, size: 35, color: Colors.blue),
                          ),
                          const SizedBox(width: 16),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  user.nomComplet ?? user.email,
                                  style: const TextStyle(
                                    fontSize: 20,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white,
                                  ),
                                ),
                                const SizedBox(height: 4),
                                Text(
                                  'Client',
                                  style: TextStyle(
                                    fontSize: 14,
                                    color: Colors.white.withOpacity(0.9),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
        
                    const SizedBox(height: 32),
        
                    // Section recherche
                    TextField(
                      decoration: InputDecoration(
                        hintText: 'Rechercher un produit...',
                        prefixIcon: const Icon(Icons.search),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                        filled: true,
                        fillColor: Colors.grey.shade100,
                      ),
                    ),
        
                    const SizedBox(height: 24),
        
                    // Liste des produits (vide pour le moment)
                    const Expanded(
                      child: Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.shopping_bag_outlined, size: 80, color: Colors.grey),
                            SizedBox(height: 16),
                            Text(
                              'Aucun produit disponible',
                              style: TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                                color: Colors.grey,
                              ),
                            ),
                            SizedBox(height: 8),
                            Text(
                              'Les produits apparaîtront ici',
                              style: TextStyle(color: Colors.grey),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              floatingActionButton: FloatingActionButton.extended(
                onPressed: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Filtres - À venir')),
                  );
                },
                icon: const Icon(Icons.filter_list),
                label: const Text('Filtres'),
                backgroundColor: Colors.blue,
              ),
            );
          }
        }
        
        Fichier : login_page.dart
      Fichier : login_page.dart
      import 'package:exemple01/views/register_page.dart';
      import 'package:flutter/material.dart';
      import 'package:provider/provider.dart';
      import '../controllers/auth_controller.dart';
      import 'dashboard_router.dart';
      
      class LoginPage extends StatefulWidget {
        const LoginPage({super.key});
      
        @override
        State<LoginPage> createState() => _LoginPageState();
      }
      
      class _LoginPageState extends State<LoginPage>
          with SingleTickerProviderStateMixin {
        final emailController = TextEditingController();
        final passwordController = TextEditingController();
        final _formKey = GlobalKey<FormState>();
        bool _obscurePassword = true;
        late AnimationController _animationController;
        late Animation<double> _opacityAnimation;
        late Animation<double> _scaleAnimation;
      
        @override
        void initState() {
          super.initState();
          _animationController = AnimationController(
            vsync: this,
            duration: const Duration(milliseconds: 1000),
          );
      
          _opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
            CurvedAnimation(
              parent: _animationController,
              curve: const Interval(0.0, 0.6, curve: Curves.easeIn),
            ),
          );
      
          _scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
            CurvedAnimation(
              parent: _animationController,
              curve: const Interval(0.2, 1.0, curve: Curves.elasticOut),
            ),
          );
      
          _animationController.forward();
        }
      
        @override
        void dispose() {
          emailController.dispose();
          passwordController.dispose();
          _animationController.dispose();
          super.dispose();
        }
      
        void _showComingSoon(BuildContext context) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Fonctionnalité à venir...'),
            ),
          );
        }
      
        Widget _buildSocialButton({
          required IconData icon,
          required Color color,
          required VoidCallback onPressed,
        }) {
          return Container(
            width: 56,
            height: 56,
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(12),
              border: Border.all(color: Colors.grey.shade300),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.05),
                  blurRadius: 10,
                  offset: const Offset(0, 2),
                ),
              ],
            ),
            child: IconButton(
              icon: Icon(icon, color: color, size: 28),
              onPressed: onPressed,
            ),
          );
        }
      
        @override
        Widget build(BuildContext context) {
          final auth = Provider.of<AuthController>(context);
          final theme = Theme.of(context);
          final isDark = theme.brightness == Brightness.dark;
          final screenWidth = MediaQuery.of(context).size.width;
      
          return Scaffold(
            body: Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: isDark
                      ? [
                          Colors.deepPurple.shade900,
                          Colors.purple.shade800,
                          Colors.indigo.shade900,
                        ]
                      : [
                          Colors.purple.shade50,
                          Colors.blue.shade50,
                          Colors.white,
                        ],
                ),
              ),
              child: SafeArea(
                child: SingleChildScrollView(
                  child: AnimatedBuilder(
                    animation: _animationController,
                    builder: (context, child) {
                      return Opacity(
                        opacity: _opacityAnimation.value,
                        child: Transform.scale(
                          scale: _scaleAnimation.value,
                          child: Padding(
                            padding: EdgeInsets.symmetric(
                              horizontal: screenWidth > 600 ? 100 : 24,
                            ),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                const SizedBox(height: 20),
      
                                // Logo/Titre
                                Center(
                                  child: Column(
                                    children: [
                                      Container(
                                        width: 80,
                                        height: 80,
                                        decoration: BoxDecoration(
                                          gradient: LinearGradient(
                                            colors: [
                                              Colors.purpleAccent,
                                              Colors.blueAccent,
                                            ],
                                            begin: Alignment.topLeft,
                                            end: Alignment.bottomRight,
                                          ),
                                          borderRadius: BorderRadius.circular(20),
                                          boxShadow: [
                                            BoxShadow(
                                              color: Colors.purple.withOpacity(0.3),
                                              blurRadius: 15,
                                              spreadRadius: 2,
                                              offset: const Offset(0, 4),
                                            ),
                                          ],
                                        ),
                                        child: const Icon(
                                          Icons.lock_outline_rounded,
                                          size: 40,
                                          color: Colors.white,
                                        ),
                                      ),
                                      const SizedBox(height: 20),
                                      Text(
                                        'Bienvenue',
                                        style: TextStyle(
                                          fontSize: 32,
                                          fontWeight: FontWeight.bold,
                                          color: isDark
                                              ? Colors.white
                                              : Colors.purple.shade900,
                                          letterSpacing: 1.2,
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      Text(
                                        'Connectez-vous à votre compte',
                                        style: TextStyle(
                                          fontSize: 16,
                                          color: isDark
                                              ? Colors.grey.shade300
                                              : Colors.grey.shade700,
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
      
                                const SizedBox(height: 32),
      
                                // Formulaire
                                Form(
                                  key: _formKey,
                                  child: Column(
                                    children: [
                                      // Champ Email
                                      Container(
                                        margin: const EdgeInsets.only(bottom: 20),
                                        decoration: BoxDecoration(
                                          borderRadius: BorderRadius.circular(15),
                                          boxShadow: [
                                            BoxShadow(
                                              color: Colors.black.withOpacity(0.05),
                                              blurRadius: 10,
                                              offset: const Offset(0, 4),
                                            ),
                                          ],
                                        ),
                                        child: TextFormField(
                                          controller: emailController,
                                          keyboardType: TextInputType.emailAddress,
                                          validator: (value) {
                                            if (value == null || value.isEmpty) {
                                              return 'Veuillez entrer votre email';
                                            }
                                            if (!value.contains('@')) {
                                              return 'Email invalide';
                                            }
                                            return null;
                                          },
                                          decoration: InputDecoration(
                                            labelText: 'Email',
                                            labelStyle: TextStyle(
                                              color: isDark
                                                  ? Colors.grey.shade400
                                                  : Colors.grey.shade600,
                                              fontWeight: FontWeight.w500,
                                            ),
                                            prefixIcon: Container(
                                              margin: const EdgeInsets.only(
                                                  right: 12, left: 4),
                                              decoration: BoxDecoration(
                                                border: Border(
                                                  right: BorderSide(
                                                    color: isDark
                                                        ? Colors.grey.shade700
                                                        : Colors.grey.shade300,
                                                    width: 1.5,
                                                  ),
                                                ),
                                              ),
                                              child: Padding(
                                                padding: const EdgeInsets.symmetric(
                                                    horizontal: 16),
                                                child: Icon(
                                                  Icons.email_outlined,
                                                  color: Colors.purple.shade400,
                                                ),
                                              ),
                                            ),
                                            filled: true,
                                            fillColor: isDark
                                                ? Colors.grey.shade900
                                                : Colors.white,
                                            border: OutlineInputBorder(
                                              borderRadius: BorderRadius.circular(15),
                                              borderSide: BorderSide.none,
                                            ),
                                            enabledBorder: OutlineInputBorder(
                                              borderRadius: BorderRadius.circular(15),
                                              borderSide: BorderSide(
                                                color: isDark
                                                    ? Colors.grey.shade800
                                                    : Colors.grey.shade300,
                                                width: 1.5,
                                              ),
                                            ),
                                            focusedBorder: OutlineInputBorder(
                                              borderRadius: BorderRadius.circular(15),
                                              borderSide: BorderSide(
                                                color: Colors.purple.shade400,
                                                width: 2,
                                              ),
                                            ),
                                            contentPadding:
                                                const EdgeInsets.symmetric(
                                              horizontal: 20,
                                              vertical: 18,
                                            ),
                                          ),
                                        ),
                                      ),
      
                                      // Champ Mot de passe
                                      Container(
                                        margin: const EdgeInsets.only(bottom: 10),
                                        decoration: BoxDecoration(
                                          borderRadius: BorderRadius.circular(15),
                                          boxShadow: [
                                            BoxShadow(
                                              color: Colors.black.withOpacity(0.05),
                                              blurRadius: 10,
                                              offset: const Offset(0, 4),
                                            ),
                                          ],
                                        ),
                                        child: TextFormField(
                                          controller: passwordController,
                                          obscureText: _obscurePassword,
                                          validator: (value) {
                                            if (value == null || value.isEmpty) {
                                              return 'Veuillez entrer votre mot de passe';
                                            }
                                            if (value.length < 6) {
                                              return 'Le mot de passe doit contenir au moins 6 caractères';
                                            }
                                            return null;
                                          },
                                          decoration: InputDecoration(
                                            labelText: 'Mot de passe',
                                            labelStyle: TextStyle(
                                              color: isDark
                                                  ? Colors.grey.shade400
                                                  : Colors.grey.shade600,
                                              fontWeight: FontWeight.w500,
                                            ),
                                            prefixIcon: Container(
                                              margin: const EdgeInsets.only(
                                                  right: 12, left: 4),
                                              decoration: BoxDecoration(
                                                border: Border(
                                                  right: BorderSide(
                                                    color: isDark
                                                        ? Colors.grey.shade700
                                                        : Colors.grey.shade300,
                                                    width: 1.5,
                                                  ),
                                                ),
                                              ),
                                              child: Padding(
                                                padding: const EdgeInsets.symmetric(
                                                    horizontal: 16),
                                                child: Icon(
                                                  Icons.lock_outline_rounded,
                                                  color: Colors.purple.shade400,
                                                ),
                                              ),
                                            ),
                                            suffixIcon: IconButton(
                                              icon: Icon(
                                                _obscurePassword
                                                    ? Icons.visibility_outlined
                                                    : Icons.visibility_off_outlined,
                                                color: Colors.grey.shade500,
                                              ),
                                              onPressed: () {
                                                setState(() {
                                                  _obscurePassword =
                                                      !_obscurePassword;
                                                });
                                              },
                                            ),
                                            filled: true,
                                            fillColor: isDark
                                                ? Colors.grey.shade900
                                                : Colors.white,
                                            border: OutlineInputBorder(
                                              borderRadius: BorderRadius.circular(15),
                                              borderSide: BorderSide.none,
                                            ),
                                            enabledBorder: OutlineInputBorder(
                                              borderRadius: BorderRadius.circular(15),
                                              borderSide: BorderSide(
                                                color: isDark
                                                    ? Colors.grey.shade800
                                                    : Colors.grey.shade300,
                                                width: 1.5,
                                              ),
                                            ),
                                            focusedBorder: OutlineInputBorder(
                                              borderRadius: BorderRadius.circular(15),
                                              borderSide: BorderSide(
                                                color: Colors.purple.shade400,
                                                width: 2,
                                              ),
                                            ),
                                            contentPadding:
                                                const EdgeInsets.symmetric(
                                              horizontal: 20,
                                              vertical: 18,
                                            ),
                                          ),
                                        ),
                                      ),
      
                                      // Mot de passe oublié
                                      Align(
                                        alignment: Alignment.centerRight,
                                        child: TextButton(
                                          onPressed: () {
                                            _showComingSoon(context);
                                          },
                                          child: Text(
                                            'Mot de passe oublié ?',
                                            style: TextStyle(
                                              color: Colors.purple.shade500,
                                              fontWeight: FontWeight.w500,
                                            ),
                                          ),
                                        ),
                                      ),
      
                                      const SizedBox(height: 16),
      
                                      // Message d'erreur
                                      if (auth.errorMessage != null)
                                        AnimatedSwitcher(
                                          duration: const Duration(milliseconds: 300),
                                          child: Container(
                                            key: ValueKey(auth.errorMessage),
                                            width: double.infinity,
                                            padding: const EdgeInsets.all(16),
                                            margin: const EdgeInsets.only(bottom: 20),
                                            decoration: BoxDecoration(
                                              color: Colors.red.shade50,
                                              borderRadius: BorderRadius.circular(12),
                                              border: Border.all(
                                                color: Colors.red.shade200,
                                                width: 1,
                                              ),
                                            ),
                                            child: Row(
                                              children: [
                                                Icon(
                                                  Icons.error_outline,
                                                  color: Colors.red.shade600,
                                                ),
                                                const SizedBox(width: 12),
                                                Expanded(
                                                  child: Text(
                                                    auth.errorMessage!,
                                                    style: TextStyle(
                                                      color: Colors.red.shade700,
                                                      fontWeight: FontWeight.w500,
                                                    ),
                                                  ),
                                                ),
                                                IconButton(
                                                  icon: Icon(
                                                    Icons.close,
                                                    color: Colors.red.shade600,
                                                    size: 20,
                                                  ),
                                                  onPressed: () => auth.clearError(),
                                                ),
                                              ],
                                            ),
                                          ),
                                        ),
      
                                      // Bouton de connexion - MODIFIÉ ICI ⬇️
                                      SizedBox(
                                        width: double.infinity,
                                        height: 56,
                                        child: ElevatedButton(
                                          onPressed: auth.isLoading
                                              ? null
                                              : () async {
                                                  if (_formKey.currentState!.validate()) {
                                                    final success = await auth.login(
                                                      emailController.text.trim(),
                                                      passwordController.text.trim(),
                                                    );
      
                                                    if (success && mounted) {
                                                      // Redirection vers le dashboard approprié selon le rôle
                                                      Navigator.pushReplacement(
                                                        context,
                                                        MaterialPageRoute(
                                                          builder: (_) => DashboardRouter(
                                                            user: auth.user!,
                                                          ),
                                                        ),
                                                      );
                                                    }
                                                  }
                                                },
                                          style: ElevatedButton.styleFrom(
                                            backgroundColor: Colors.purple.shade500,
                                            foregroundColor: Colors.white,
                                            elevation: 6,
                                            shadowColor:
                                                Colors.purple.withOpacity(0.3),
                                            shape: RoundedRectangleBorder(
                                              borderRadius: BorderRadius.circular(15),
                                            ),
                                            padding: const EdgeInsets.symmetric(
                                                vertical: 16),
                                          ),
                                          child: auth.isLoading
                                              ? SizedBox(
                                                  width: 24,
                                                  height: 24,
                                                  child: CircularProgressIndicator(
                                                    strokeWidth: 2.5,
                                                    valueColor:
                                                        AlwaysStoppedAnimation<Color>(
                                                      Colors.white,
                                                    ),
                                                  ),
                                                )
                                              : Row(
                                                  mainAxisAlignment:
                                                      MainAxisAlignment.center,
                                                  children: [
                                                    Text(
                                                      'Se connecter',
                                                      style: TextStyle(
                                                        fontSize: 17,
                                                        fontWeight: FontWeight.w600,
                                                        letterSpacing: 0.5,
                                                      ),
                                                    ),
                                                    const SizedBox(width: 10),
                                                    const Icon(
                                                      Icons.arrow_forward_rounded,
                                                      size: 20,
                                                    ),
                                                  ],
                                                ),
                                        ),
                                      ),
      
                                      const SizedBox(height: 20),
      
                                      // Séparateur
                                      Row(
                                        children: [
                                          Expanded(
                                            child: Divider(
                                              color: isDark
                                                  ? Colors.grey.shade800
                                                  : Colors.grey.shade300,
                                              thickness: 1,
                                            ),
                                          ),
                                          Padding(
                                            padding: const EdgeInsets.symmetric(
                                                horizontal: 16),
                                            child: Text(
                                              'Ou continuer avec',
                                              style: TextStyle(
                                                color: isDark
                                                    ? Colors.grey.shade500
                                                    : Colors.grey.shade600,
                                                fontSize: 14,
                                              ),
                                            ),
                                          ),
                                          Expanded(
                                            child: Divider(
                                              color: isDark
                                                  ? Colors.grey.shade800
                                                  : Colors.grey.shade300,
                                              thickness: 1,
                                            ),
                                          ),
                                        ],
                                      ),
      
                                      const SizedBox(height: 16),
      
                                      // Boutons de connexion sociale
                                      Row(
                                        mainAxisAlignment: MainAxisAlignment.center,
                                        children: [
                                          _buildSocialButton(
                                            icon: Icons.facebook,
                                            color: const Color(0xFF1877F2),
                                            onPressed: () {
                                              _showComingSoon(context);
                                            },
                                          ),
                                          const SizedBox(width: 20),
                                          _buildSocialButton(
                                            icon: Icons.g_mobiledata,
                                            color: const Color(0xFFDB4437),
                                            onPressed: () {
                                              _showComingSoon(context);
                                            },
                                          ),
                                          const SizedBox(width: 20),
                                          _buildSocialButton(
                                            icon: Icons.apple,
                                            color: Colors.black,
                                            onPressed: () {
                                              _showComingSoon(context);
                                            },
                                          ),
                                        ],
                                      ),
      
                                      const SizedBox(height: 24),
      
                                      // Lien vers inscription
                                      Center(
                                        child: Row(
                                          mainAxisAlignment: MainAxisAlignment.center,
                                          children: [
                                            Text(
                                              'Pas encore de compte ? ',
                                              style: TextStyle(
                                                color: isDark
                                                    ? Colors.grey.shade400
                                                    : Colors.grey.shade600,
                                                fontSize: 15,
                                              ),
                                            ),
                                            GestureDetector(
                                              onTap: () {
                                                Navigator.push(
                                                  context,
                                                  MaterialPageRoute(
                                                      builder: (_) =>
                                                          const RegisterPage()),
                                                );
                                              },
                                              child: Text(
                                                'S\'inscrire',
                                                style: TextStyle(
                                                  color: Colors.purple.shade500,
                                                  fontWeight: FontWeight.bold,
                                                  fontSize: 15,
                                                ),
                                              ),
                                            ),
                                          ],
                                        ),
                                      ),
      
                                      const SizedBox(height: 20),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      );
                    },
                  ),
                ),
              ),
            ),
          );
        }
      }
    10. Description : Ce fichier gère l’interface et la logique de connexion utilisateur.
      • L’utilisateur saisit son email et mot de passe.
      • L’application envoie une requête POST au script PHP login.php.
      • Si les identifiants sont corrects, l’utilisateur est redirigé vers la HomePage.
      • En cas d’erreur, un message d’alerte s’affiche (ex. identifiants invalides).
      Fichier : home.dart
      import 'dart:convert';
      import 'package:flutter/material.dart';
      import 'package:http/http.dart' as http;
      import 'client_model.dart';
      
      class HomePage extends StatefulWidget {
        const HomePage({super.key});
      
        @override
        State<HomePage> createState() => _HomePageState();
      }
      
      class _HomePageState extends State<HomePage> {
        final _nameCtrl = TextEditingController();
        final _familyCtrl = TextEditingController();
        final _ageCtrl = TextEditingController();
      
        String serverMsg = '';
        final List<Client> _clientsList = [];
      
        final String baseUrl = "http://10.0.2.2/flutter/mon_api_php/client"; // Utilisation de 127.0.0.1 pour fonctionner sur navigateur
      
        Future<void> _postRequest() async {
          try {
            final response = await http.post(
              Uri.parse("$baseUrl/insert.php"),
              headers: {'Content-Type': 'application/json'},
              body: jsonEncode({
                'name': _nameCtrl.text,
                'family': _familyCtrl.text,
                'age': int.tryParse(_ageCtrl.text) ?? 0,
              }),
            );
      
            _clearForm();
      
            if (response.statusCode == 200) {
              serverMsg = "Data Successfully Inserted";
              _getRequest();
            } else {
              serverMsg = "Insert Failed";
            }
          } catch (e) {
            serverMsg = e.toString();
          }
          setState(() {});
        }
      
        Future<void> _getRequest() async {
          try {
            final response = await http.get(Uri.parse("$baseUrl/get.php"));
      
            if (response.statusCode == 200) {
              final List jsonData = jsonDecode(response.body);
              _clientsList.clear();
              _clientsList.addAll(jsonData.map((e) => Client.fromJson(e)));
              serverMsg = "Data Loaded";
            } else {
              serverMsg = "Failed to Load";
            }
          } catch (e) {
            serverMsg = e.toString();
          }
          setState(() {});
        }
      
        Future<void> _deleteRequest(int id) async {
          try {
            final response = await http.post(
              Uri.parse("$baseUrl/delete.php"),
              headers: {'Content-Type': 'application/json'},
              body: jsonEncode({'id': id}),
            );
      
            if (response.statusCode == 200) {
              serverMsg = "Deleted Successfully";
              _getRequest();
            } else {
              serverMsg = "Delete Failed";
            }
          } catch (e) {
            serverMsg = e.toString();
          }
          setState(() {});
        }
      
        Future<void> _updateRequest(Client client) async {
          try {
            final response = await http.post(
              Uri.parse("$baseUrl/update.php"),
              headers: {'Content-Type': 'application/json'},
              body: jsonEncode(client.toJson()),
            );
      
            if (response.statusCode == 200) {
              serverMsg = "Updated Successfully";
              _getRequest();
            } else {
              serverMsg = "Update Failed";
            }
          } catch (e) {
            serverMsg = e.toString();
          }
          setState(() {});
        }
      
        void _clearForm() {
          _nameCtrl.clear();
          _familyCtrl.clear();
          _ageCtrl.clear();
        }
      
        @override
        void initState() {
          super.initState();
          _getRequest();
        }
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(title: const Text("CRUD Example")),
            body: SafeArea(
              child: Column(
                children: [
                  Container(
                    height: 300,
                    color: Colors.grey[200],
                    padding: const EdgeInsets.all(20),
                    child: Column(
                      children: [
                        TextField(
                          controller: _nameCtrl,
                          decoration: const InputDecoration(labelText: 'Name'),
                        ),
                        TextField(
                          controller: _familyCtrl,
                          decoration: const InputDecoration(labelText: 'Family'),
                        ),
                        TextField(
                          controller: _ageCtrl,
                          decoration: const InputDecoration(labelText: 'Age'),
                          keyboardType: TextInputType.number,
                        ),
                        const SizedBox(height: 16),
                        Text(
                          serverMsg,
                          style: const TextStyle(color: Colors.blue),
                        ),
                      ],
                    ),
                  ),
                  Expanded(
                    child: ListView.builder(
                      itemCount: _clientsList.length,
                      itemBuilder: (context, index) {
                        final client = _clientsList[index];
                        return ListTile(
                          title: Text("${client.name} ${client.family}"),
                          subtitle: Text("Age: ${client.age}"),
                          trailing: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              IconButton(
                                icon: const Icon(Icons.edit, color: Colors.green),
                                onPressed: () {
                                  _nameCtrl.text = client.name;
                                  _familyCtrl.text = client.family;
                                  _ageCtrl.text = client.age.toString();
      
                                  // Show dialog to update client
                                  showDialog(
                                    context: context,
                                    builder: (_) {
                                      return AlertDialog(
                                        title: Text("Modifier le client"),
                                        content: Column(
                                          mainAxisSize: MainAxisSize.min,
                                          children: [
                                            TextField(
                                              controller: _nameCtrl,
                                              decoration: const InputDecoration(labelText: "Name"),
                                            ),
                                            TextField(
                                              controller: _familyCtrl,
                                              decoration: const InputDecoration(labelText: "Family"),
                                            ),
                                            TextField(
                                              controller: _ageCtrl,
                                              decoration: const InputDecoration(labelText: "Age"),
                                            ),
                                          ],
                                        ),
                                        actions: [
                                          TextButton(
                                            child: const Text("Annuler"),
                                            onPressed: () => Navigator.pop(context),
                                          ),
                                          ElevatedButton(
                                            child: const Text("Mettre à jour"),
                                            onPressed: () {
                                              _updateRequest(Client(
                                                id: client.id,
                                                name: _nameCtrl.text,
                                                family: _familyCtrl.text,
                                                age: int.tryParse(_ageCtrl.text) ?? 0,
                                              ));
                                              Navigator.pop(context);
                                            },
                                          ),
                                        ],
                                      );
                                    },
                                  );
                                },
                              ),
                              IconButton(
                                icon: const Icon(Icons.delete, color: Colors.red),
                                onPressed: () {
                                  _deleteRequest(client.id);
                                },
                              ),
                            ],
                          ),
                        );
                      },
                    ),
                  ),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: _postRequest,
              child: const Icon(Icons.add),
            ),
          );
        }
      }
      
    11. Description : Ce fichier gère la page principale après connexion.
      • Il affiche une liste ou un tableau de données récupérées depuis la base MySQL via les scripts PHP (GET/POST/CRUD).
      • Il permet de naviguer vers d’autres fonctionnalités (ajout, modification, suppression).
      • Il contient la logique pour actualiser l’interface après les opérations CRUD.
    12. Étape 5 : Définir un modèle de données (Client)
        Fichier : client_model.dart
        class Client {
          final int id;
          final String name;
          final String family;
          final int age;
        
          Client({
            required this.id,
            required this.name,
            required this.family,
            required this.age,
          });
        
          factory Client.fromJson(Map<String, dynamic> json) {
            return Client(
              id: int.parse(json['id'].toString()),
              name: json['name'],
              family: json['family'],
              age: int.parse(json['age'].toString()),
            );
          }
        
          Map<String, dynamic> toJson() {
            return {'id': id, 'name': name, 'family': family, 'age': age};
          }
        }
      • Description : Cette classe représente un objet Client. Elle fournit une méthode fromJson pour transformer une réponse JSON en objet Dart, et une méthode toJson pour envoyer les données dans le bon format au serveur.
  5. CRUD (client)

    1. Créer la table client:
        CREATE TABLE `client` (
          `id` int NOT NULL AUTO_INCREMENT,
          `name` varchar(100) NOT NULL,
          `family` varchar(100) NOT NULL,
          `age` int NOT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 ;
    2. Étape 1 : Créer le fichier get_client.php
      • Ce fichier récupère la liste des clients depuis la base de données au format JSON. Il est utilisé par l'application Flutter pour afficher les données clients.
      • Fichier get_client.php
        <?php
        header("Access-Control-Allow-Origin: *");
        header("Content-Type: application/json");
        
        require_once('../dbconnect.php');
        
        $stmt = $pdo->query("SELECT * FROM client ORDER BY id DESC");
        $clients = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        echo json_encode($clients);
        
    3. Étape 2 : Créer le fichier insert_client.php
      • Ce fichier permet d’ajouter un nouveau client à la base de données. Il reçoit les données (nom, email, etc.) en POST depuis l’application Flutter.
      • Fichier insert_client.php
        <?php
        // Autorisations CORS
        header("Access-Control-Allow-Origin: *");
        header("Access-Control-Allow-Methods: POST, OPTIONS");
        header("Access-Control-Allow-Headers: Content-Type");
        header("Content-Type: application/json");
        
        // Gestion de la requête OPTIONS (pré-vol CORS)
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(200);
            exit;
        }
        
        require_once('../dbconnect.php');
        
        // Lecture des données JSON envoyées
        $data = json_decode(file_get_contents("php://input"));
        
        // Sécurité basique : initialisation
        $name = $data->name ?? '';
        $family = $data->family ?? '';
        $age = $data->age ?? 0;
        
        // Vérification des champs requis
        if (empty($name) || empty($family)) {
            echo json_encode(['success' => false, 
            'message' => 'Champs manquants']);
            exit;
        }
        
        // Requête d'insertion préparée
        $stmt = $pdo->prepare("INSERT INTO 
        client (name, family, age) VALUES (?, ?, ?)");
        $success = $stmt->execute([$name, $family, $age]);
        
        // Réponse JSON
        echo json_encode([
            'success' => $success,
            'message' => $success ? 'Ajouté avec succès' : 'Erreur insertion'
        ]);
        
    4. Étape 3 : Créer le fichier update_client.php
      • Ce fichier met à jour les informations d’un client existant dans la base de données. L'application Flutter envoie l’identifiant du client et les nouvelles données via POST.
      • Fichier: update_client.php
        <?php
        // Autorisations CORS
        header("Access-Control-Allow-Origin: *");
        header("Access-Control-Allow-Methods: POST, OPTIONS");
        header("Access-Control-Allow-Headers: Content-Type");
        header("Content-Type: application/json");
        
        // Gestion de la requête préliminaire OPTIONS (pré-vol CORS)
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(200);
            exit;
        }
        
        require_once('../dbconnect.php');
        
        // Lecture des données JSON envoyées
        $data = json_decode(file_get_contents("php://input"));
        
        $id = $data->id ?? 0;
        $name = $data->name ?? '';
        $family = $data->family ?? '';
        $age = $data->age ?? 0;
        
        // Vérification de l'ID
        if ($id <= 0) {
            echo json_encode(['success' => false, 'message' => 'ID invalide']);
            exit;
        }
        
        // Requête SQL préparée
        $stmt = $pdo->prepare("UPDATE client SET name = ?, family = ?, age = ? WHERE id = ?");
        $success = $stmt->execute([$name, $family, $age, $id]);
        
        // Réponse
        echo json_encode([
            'success' => $success,
            'message' => $success ? 'Mis à jour avec succès' : 'Échec de la mise à jour'
        ]);
        
    5. Étape 4 : Créer le fichier delete_client.php
      • Ce fichier supprime un client de la base de données. L'application Flutter envoie l’identifiant du client à supprimer via une requête POST.
      • Fichier :delete_client.php
        <?php
        header("Access-Control-Allow-Origin: *");
        header("Content-Type: application/json");
        
        require_once('../dbconnect.php');
        
        $data = json_decode(file_get_contents("php://input"));
        
        $id = $data->id ?? 0;
        
        if ($id <= 0) {
            echo json_encode(['success' => false, 'message' => 'ID invalide']);
            exit;
        }
        
        $stmt = $pdo->prepare("DELETE FROM client WHERE id = ?");
        $success = $stmt->execute([$id]);
        
        echo json_encode([
            'success' => $success,
            'message' => $success ? 'Supprimé avec succès' : 'Erreur de suppression'
        ]);
        
    6. Étape 5 : Créer le fichier home_page.dart
        Fichier: home_page.dart
        import 'dart:convert';
        import 'package:exemple01/login_page.dart';
        import 'package:flutter/material.dart';
        import 'package:http/http.dart' as http;
        import 'client_model.dart';
        
        class HomePage extends StatefulWidget {
          const HomePage({super.key});
        
          @override
          State<HomePage> createState() => _HomePageState();
        }
        
        class _HomePageState extends State<HomePage> {
          final _nameCtrl = TextEditingController();
          final _familyCtrl = TextEditingController();
          final _ageCtrl = TextEditingController();
        
          String serverMsg = '';
          final List<Client> _clientsList = [];
        
          final String baseUrl = "http://10.0.2.2/flutter/mon_api_php/client";
        
          // ----------- INSERT ------------
          Future<void> _postRequest() async {
            try {
              final response = await http.post(
                Uri.parse("$baseUrl/insert.php"),
                headers: {'Content-Type': 'application/json'},
                body: jsonEncode({
                  'name': _nameCtrl.text,
                  'family': _familyCtrl.text,
                  'age': int.tryParse(_ageCtrl.text) ?? 0,
                }),
              );
        
              _clearForm();
        
              serverMsg =
                  response.statusCode == 200 ? "Client ajouté" : "Échec d'ajout";
        
              await _getRequest();
              setState(() {});
            } catch (e) {
              serverMsg = e.toString();
              setState(() {});
            }
          }
        
          // ----------- LOAD LIST ------------
          Future<void> _getRequest() async {
            try {
              final response = await http.get(Uri.parse("$baseUrl/get.php"));
        
              if (response.statusCode == 200) {
                final List jsonData = jsonDecode(response.body);
                _clientsList.clear();
                _clientsList.addAll(jsonData.map((e) => Client.fromJson(e)));
                serverMsg = "Données chargées";
              } else {
                serverMsg = "Erreur de chargement";
              }
            } catch (e) {
              serverMsg = e.toString();
            }
            setState(() {});
          }
        
          // ----------- DELETE ------------
          Future<void> _deleteRequest(int id) async {
            try {
              final response = await http.post(
                Uri.parse("$baseUrl/delete.php"),
                headers: {'Content-Type': 'application/json'},
                body: jsonEncode({'id': id}),
              );
        
              serverMsg =
                  response.statusCode == 200 ? "Supprimé" : "Échec de suppression";
        
              await _getRequest();
              setState(() {});
            } catch (e) {
              serverMsg = e.toString();
              setState(() {});
            }
          }
        
          // ----------- UPDATE ------------
          Future<void> _updateRequest(Client client) async {
            try {
              final response = await http.post(
                Uri.parse("$baseUrl/update.php"),
                headers: {'Content-Type': 'application/json'},
                body: jsonEncode(client.toJson()),
              );
        
              serverMsg =
                  response.statusCode == 200 ? "Modifié" : "Échec de modification";
        
              await _getRequest();
              setState(() {});
            } catch (e) {
              serverMsg = e.toString();
              setState(() {});
            }
          }
        
          // ----------- CLEAR FORM ------------
          void _clearForm() {
            _nameCtrl.clear();
            _familyCtrl.clear();
            _ageCtrl.clear();
          }
        
          @override
          void initState() {
            super.initState();
            _getRequest();
          }
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              backgroundColor: Colors.grey[100],
        
              // ──────────────────── APPBAR ─────────────────────
              appBar: AppBar(
                leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            Navigator.pushReplacement(
              context,
              MaterialPageRoute(builder: (_) => LoginPage()),
            );
          },
        ),
                title: const Text("Gestion des Clients"),
                backgroundColor: Colors.indigo[600],
                actions: [
                  IconButton(
                    icon: const Icon(Icons.add),
                    onPressed: () {
                      _clearForm();
                      _showAddDialog();
                    },
                  )
                ],
              ),
        
              // ──────────────────── BODY : LIST ONLY ─────────────────────
              body: _buildClientList(),
              // --- Bouton flottant en bas à droite ---
          floatingActionButton: FloatingActionButton(
            onPressed: () {
                      _clearForm();
                      _showAddDialog();
                    },
            backgroundColor: Colors.indigo,
            child: const Icon(Icons.add),
          ),
        
          floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
        
            );
          }
        
          // ──────────────────── LIST ─────────────────────
          Widget _buildClientList() {
            return ListView.builder(
              padding: const EdgeInsets.all(12),
              itemCount: _clientsList.length,
              itemBuilder: (context, index) {
                final client = _clientsList[index];
                return Card(
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
                  margin: const EdgeInsets.symmetric(vertical: 8),
                  elevation: 3,
                  child: ListTile(
                    title: Text(
                      "${client.name} ${client.family}",
                      style: const TextStyle(fontWeight: FontWeight.w600),
                    ),
                    subtitle: Text("Âge : ${client.age}"),
                    trailing: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        IconButton(
                          icon: const Icon(Icons.edit, color: Colors.indigo),
                          onPressed: () {
                            _nameCtrl.text = client.name;
                            _familyCtrl.text = client.family;
                            _ageCtrl.text = client.age.toString();
                            _showUpdateDialog(client);
                          },
                        ),
                        IconButton(
                          icon: const Icon(Icons.delete, color: Colors.red),
                          onPressed: () => _deleteRequest(client.id),
                        ),
                      ],
                    ),
                  ),
                );
                
              },
              
            );
            
          }
        
          // ──────────────────── POPUP AJOUT ─────────────────────
          void _showAddDialog() {
            showDialog(
              context: context,
              builder: (_) {
                return AlertDialog(
                  title: const Text("Ajouter un client"),
                  content: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      _textField(_nameCtrl, "Nom", Icons.person),
                      const SizedBox(height: 8),
                      _textField(_familyCtrl, "Prénom", Icons.family_restroom),
                      const SizedBox(height: 8),
                      _textField(_ageCtrl, "Âge", Icons.cake,
                          keyboard: TextInputType.number),
                    ],
                  ),
                  actions: [
                    TextButton(
                      child: const Text("Annuler"),
                      onPressed: () => Navigator.pop(context),
                    ),
                    ElevatedButton(
                      child: const Text("Ajouter"),
                      onPressed: () async {
                        await _postRequest();
                        Navigator.pop(context);
                      },
                    ),
                  ],
                );
              },
            );
          }
        
          // ──────────────────── POPUP MODIFICATION ─────────────────────
          void _showUpdateDialog(Client client) {
            showDialog(
              context: context,
              builder: (_) {
                return AlertDialog(
                  title: const Text("Modifier le client"),
                  content: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      _textField(_nameCtrl, "Nom", Icons.person),
                      const SizedBox(height: 8),
                      _textField(_familyCtrl, "Prénom", Icons.family_restroom),
                      const SizedBox(height: 8),
                      _textField(_ageCtrl, "Âge", Icons.cake,
                          keyboard: TextInputType.number),
                    ],
                  ),
                  actions: [
                    TextButton(
                      child: const Text("Annuler"),
                      onPressed: () => Navigator.pop(context),
                    ),
                    ElevatedButton(
                      child: const Text("Mettre à jour"),
                      onPressed: () async {
                        await _updateRequest(
                          Client(
                            id: client.id,
                            name: _nameCtrl.text,
                            family: _familyCtrl.text,
                            age: int.tryParse(_ageCtrl.text) ?? 0,
                          ),
                        );
                        Navigator.pop(context);
                      },
                    ),
                  ],
                );
              },
            );
          }
        
          // ──────────────────── TEXT FIELD ─────────────────────
          Widget _textField(TextEditingController controller, String label, IconData icon,
              {TextInputType keyboard = TextInputType.text}) {
            return TextField(
              controller: controller,
              keyboardType: keyboard,
              decoration: InputDecoration(
                labelText: label,
                prefixIcon: Icon(icon),
                filled: true,
                fillColor: Colors.grey[50],
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
            );
          }
        }
  6. Exercices

    • Exercice 1 : Créer une page d’inscription Flutter + script PHP register.php.
    • Exercice 2 : Ajouter le hachage de mot de passe avec password_hash() côté PHP.
    • Exercice 3 : Créer une page de profil affichant l’email après connexion.
  7. Fichiers réalisés

    1. connexion.php
    2. login.php
    3. main.dart
    4. pubspec.yaml
    5. Base de données MySQL : base_api_php / table users