Se connecter à Flutter avec PHP MySQL
Se connecter à Flutter avec PHP MySQL
-
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.
-
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.
-
Règles et bonnes pratiques pour Flutter ↔ PHP MySQL
-
Côté PHP :
- Toujours définir les
en-têtes CORSpour autoriser les requêtes Flutter : - 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.
-
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).
-
Architecture CRUD Flutter ↔ PHP MySQL
-
Configuration du projet
-
Étape 1 : Créer la base de données MySQL
- Créer la base de données :
- Créer la table
utulisateurs: - Créer la table
Ajouter des utulisateurs: - Ajouter un utilisateur de test en exécutant le fichier add_user.php:
-
Étape 2 : Créer les scripts PHP
-
Étape 3 : Créer le projet Flutter
- Créer le projet :
- Ajouter la dépendance HTTP dans
pubspec.yaml: -
Architecture du projet (Dart / Flutter)
-
Étape 4 : Les fichiers Controllers/
- Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget
MainAppqui affiche directement la page de connexionLoginPage. -
Dossier ‘Controllers’
-
Dossier ‘Models’
-
Dossier ‘Services’
-
Dossier ‘Views’
- 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).
- 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.
-
Étape 5 : Définir un modèle de données (Client)
- Description : Cette classe représente un objet
Client. Elle fournit une méthodefromJsonpour transformer une réponse JSON en objet Dart, et une méthodetoJsonpour envoyer les données dans le bon format au serveur. -
CRUD (client)
-
Créer la table
client: -
É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.
-
É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.
-
É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.
-
É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.
-
Étape 5 : Créer le fichier
home_page.dart -
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.
-
Fichiers réalisés
- connexion.php
- login.php
- main.dart
- pubspec.yaml
- Base de données MySQL : base_api_php / table users
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json");
| 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() |
CREATE DATABASE base_api_php;
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;
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é.";
?>
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);
?>
flutter create exemple01_php
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
-
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
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(),
),
);
}
}
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();
}
}
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';
}
}
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');
}
}
}
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),
],
),
),
],
),
),
),
);
},
),
),
),
),
);
}
}
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),
),
);
}
}
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};
}
}
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 ;
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);
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'
]);
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'
]);
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'
]);
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),
),
),
);
}
}
