20 changed files with 2207 additions and 1003 deletions
@ -0,0 +1,16 @@ |
|||
class Permission { |
|||
final int? id; |
|||
final String name; |
|||
|
|||
Permission({this.id, required this.name}); |
|||
|
|||
factory Permission.fromMap(Map<String, dynamic> map) => Permission( |
|||
id: map['id'], |
|||
name: map['name'], |
|||
); |
|||
|
|||
Map<String, dynamic> toMap() => { |
|||
'id': id, |
|||
'name': name, |
|||
}; |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
class Role { |
|||
final int? id; |
|||
final String designation; |
|||
|
|||
Role({this.id, required this.designation}); |
|||
|
|||
Map<String, dynamic> toMap() { |
|||
return { |
|||
'id': id, |
|||
'designation': designation, |
|||
}; |
|||
} |
|||
|
|||
factory Role.fromMap(Map<String, dynamic> map) { |
|||
return Role( |
|||
id: map['id'], |
|||
designation: map['designation'], |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,477 @@ |
|||
import 'dart:async'; |
|||
import 'dart:io'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:path/path.dart'; |
|||
import 'package:path_provider/path_provider.dart'; |
|||
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; |
|||
import '../Models/users.dart'; |
|||
import '../Models/role.dart'; |
|||
import '../Models/Permission.dart'; |
|||
|
|||
class AppDatabase { |
|||
static final AppDatabase instance = AppDatabase._init(); |
|||
late Database _database; |
|||
|
|||
AppDatabase._init() { |
|||
sqfliteFfiInit(); |
|||
} |
|||
|
|||
Future<Database> get database async { |
|||
if (_database.isOpen) return _database; |
|||
_database = await _initDB('app_database.db'); |
|||
return _database; |
|||
} |
|||
|
|||
Future<void> initDatabase() async { |
|||
_database = await _initDB('app_database.db'); |
|||
await _createDB(_database, 1); |
|||
await insertDefaultPermissions(); |
|||
await insertDefaultRoles(); |
|||
await insertDefaultSuperAdmin(); |
|||
} |
|||
|
|||
Future<Database> _initDB(String filePath) async { |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, filePath); |
|||
|
|||
bool dbExists = await File(path).exists(); |
|||
if (!dbExists) { |
|||
// Optionnel : copier depuis assets si vous avez une DB pré-remplie |
|||
try { |
|||
ByteData data = await rootBundle.load('assets/database/$filePath'); |
|||
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); |
|||
await File(path).writeAsBytes(bytes); |
|||
} catch (e) { |
|||
// Si pas de fichier dans assets, on continue avec une DB vide |
|||
print('Pas de fichier DB dans assets, création d\'une nouvelle DB'); |
|||
} |
|||
} |
|||
|
|||
return await databaseFactoryFfi.openDatabase(path); |
|||
} |
|||
|
|||
Future<void> _createDB(Database db, int version) async { |
|||
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); |
|||
final tableNames = tables.map((row) => row['name'] as String).toList(); |
|||
|
|||
// Table des rôles |
|||
if (!tableNames.contains('roles')) { |
|||
await db.execute(''' |
|||
CREATE TABLE roles ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
designation TEXT NOT NULL UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'roles' créée."); |
|||
} |
|||
|
|||
// Table des permissions |
|||
if (!tableNames.contains('permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE permissions ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'permissions' créée."); |
|||
} |
|||
|
|||
// Table de liaison role_permissions |
|||
if (!tableNames.contains('role_permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE role_permissions ( |
|||
role_id INTEGER, |
|||
permission_id INTEGER, |
|||
PRIMARY KEY (role_id, permission_id), |
|||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'role_permissions' créée."); |
|||
} |
|||
|
|||
// Table des utilisateurs |
|||
if (!tableNames.contains('users')) { |
|||
await db.execute(''' |
|||
CREATE TABLE users ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL, |
|||
lastname TEXT NOT NULL, |
|||
email TEXT NOT NULL UNIQUE, |
|||
password TEXT NOT NULL, |
|||
username TEXT NOT NULL UNIQUE, |
|||
role_id INTEGER NOT NULL, |
|||
FOREIGN KEY (role_id) REFERENCES roles(id) |
|||
) |
|||
'''); |
|||
print("Table 'users' créée."); |
|||
} |
|||
} |
|||
|
|||
// ========== INSERTION DES DONNÉES PAR DÉFAUT ========== |
|||
|
|||
Future<void> insertDefaultPermissions() async { |
|||
final db = await database; |
|||
final existing = await db.query('permissions'); |
|||
if (existing.isEmpty) { |
|||
await db.insert('permissions', {'name': 'view'}); |
|||
await db.insert('permissions', {'name': 'create'}); |
|||
await db.insert('permissions', {'name': 'update'}); |
|||
await db.insert('permissions', {'name': 'delete'}); |
|||
await db.insert('permissions', {'name': 'admin'}); |
|||
print("Permissions par défaut insérées"); |
|||
} |
|||
} |
|||
|
|||
Future<void> insertDefaultRoles() async { |
|||
final db = await database; |
|||
final existingRoles = await db.query('roles'); |
|||
|
|||
if (existingRoles.isEmpty) { |
|||
// Créer le rôle Super Admin |
|||
int superAdminRoleId = await db.insert('roles', {'designation': 'Super Admin'}); |
|||
|
|||
// Créer d'autres rôles de base |
|||
int adminRoleId = await db.insert('roles', {'designation': 'Admin'}); |
|||
int userRoleId = await db.insert('roles', {'designation': 'User'}); |
|||
|
|||
// Assigner toutes les permissions au Super Admin |
|||
final permissions = await db.query('permissions'); |
|||
for (var permission in permissions) { |
|||
await db.insert('role_permissions', { |
|||
'role_id': superAdminRoleId, |
|||
'permission_id': permission['id'], |
|||
}); |
|||
} |
|||
|
|||
// Assigner quelques permissions à l'Admin |
|||
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']); |
|||
final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']); |
|||
final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']); |
|||
|
|||
if (viewPermission.isNotEmpty) { |
|||
await db.insert('role_permissions', { |
|||
'role_id': adminRoleId, |
|||
'permission_id': viewPermission.first['id'], |
|||
}); |
|||
} |
|||
if (createPermission.isNotEmpty) { |
|||
await db.insert('role_permissions', { |
|||
'role_id': adminRoleId, |
|||
'permission_id': createPermission.first['id'], |
|||
}); |
|||
} |
|||
if (updatePermission.isNotEmpty) { |
|||
await db.insert('role_permissions', { |
|||
'role_id': adminRoleId, |
|||
'permission_id': updatePermission.first['id'], |
|||
}); |
|||
} |
|||
|
|||
// Assigner seulement la permission view à User |
|||
if (viewPermission.isNotEmpty) { |
|||
await db.insert('role_permissions', { |
|||
'role_id': userRoleId, |
|||
'permission_id': viewPermission.first['id'], |
|||
}); |
|||
} |
|||
|
|||
print("Rôles par défaut créés et permissions assignées"); |
|||
} |
|||
} |
|||
|
|||
Future<void> insertDefaultSuperAdmin() async { |
|||
final db = await database; |
|||
|
|||
// Vérifier si un super admin existe déjà |
|||
final existingSuperAdmin = await db.rawQuery(''' |
|||
SELECT u.* FROM users u |
|||
INNER JOIN roles r ON u.role_id = r.id |
|||
WHERE r.designation = 'Super Admin' |
|||
'''); |
|||
|
|||
if (existingSuperAdmin.isEmpty) { |
|||
// Récupérer l'ID du rôle Super Admin |
|||
final superAdminRole = await db.query('roles', |
|||
where: 'designation = ?', |
|||
whereArgs: ['Super Admin'] |
|||
); |
|||
|
|||
if (superAdminRole.isNotEmpty) { |
|||
final superAdminRoleId = superAdminRole.first['id'] as int; |
|||
|
|||
// Créer l'utilisateur Super Admin |
|||
await db.insert('users', { |
|||
'name': 'Super', |
|||
'lastname': 'Admin', |
|||
'email': 'superadmin@youmazgestion.com', |
|||
'password': 'admin123', // CHANGEZ CE MOT DE PASSE ! |
|||
'username': 'superadmin', |
|||
'role_id': superAdminRoleId, |
|||
}); |
|||
|
|||
print("Super Admin créé avec succès !"); |
|||
print("Username: superadmin"); |
|||
print("Password: admin123"); |
|||
print("ATTENTION: Changez ce mot de passe après la première connexion !"); |
|||
} |
|||
} else { |
|||
print("Super Admin existe déjà"); |
|||
} |
|||
} |
|||
|
|||
// ========== GESTION DES UTILISATEURS ========== |
|||
|
|||
Future<int> createUser(Users user) async { |
|||
final db = await database; |
|||
return await db.insert('users', user.toMap()); |
|||
} |
|||
|
|||
Future<int> deleteUser(int id) async { |
|||
final db = await database; |
|||
return await db.delete('users', where: 'id = ?', whereArgs: [id]); |
|||
} |
|||
|
|||
Future<int> updateUser(Users user) async { |
|||
final db = await database; |
|||
return await db.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]); |
|||
} |
|||
|
|||
Future<int> getUserCount() async { |
|||
final db = await database; |
|||
List<Map<String, dynamic>> result = await db.rawQuery('SELECT COUNT(*) as count FROM users'); |
|||
return result.first['count'] as int; |
|||
} |
|||
|
|||
Future<bool> verifyUser(String username, String password) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.id |
|||
FROM users |
|||
WHERE users.username = ? AND users.password = ? |
|||
''', [username, password]); |
|||
return result.isNotEmpty; |
|||
} |
|||
|
|||
Future<Users> getUser(String username) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.*, roles.designation as role_name |
|||
FROM users |
|||
INNER JOIN roles ON users.role_id = roles.id |
|||
WHERE users.username = ? |
|||
''', [username]); |
|||
|
|||
if (result.isNotEmpty) { |
|||
return Users.fromMap(result.first); |
|||
} else { |
|||
throw Exception('User not found'); |
|||
} |
|||
} |
|||
|
|||
Future<Map<String, dynamic>?> getUserCredentials(String username, String password) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id |
|||
FROM users |
|||
INNER JOIN roles ON users.role_id = roles.id |
|||
WHERE username = ? AND password = ? |
|||
''', [username, password]); |
|||
|
|||
if (result.isNotEmpty) { |
|||
return { |
|||
'id': result.first['id'], |
|||
'username': result.first['username'] as String, |
|||
'role': result.first['role_name'] as String, |
|||
'role_id': result.first['role_id'], |
|||
}; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
Future<List<Users>> getAllUsers() async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT users.*, roles.designation as role_name |
|||
FROM users |
|||
INNER JOIN roles ON users.role_id = roles.id |
|||
ORDER BY users.id ASC |
|||
'''); |
|||
return result.map((json) => Users.fromMap(json)).toList(); |
|||
} |
|||
|
|||
// ========== GESTION DES RÔLES ========== |
|||
|
|||
Future<int> createRole(Role role) async { |
|||
final db = await database; |
|||
return await db.insert('roles', role.toMap()); |
|||
} |
|||
|
|||
Future<List<Role>> getRoles() async { |
|||
final db = await database; |
|||
final maps = await db.query('roles', orderBy: 'designation ASC'); |
|||
return List.generate(maps.length, (i) => Role.fromMap(maps[i])); |
|||
} |
|||
|
|||
Future<int> updateRole(Role role) async { |
|||
final db = await database; |
|||
return await db.update( |
|||
'roles', |
|||
role.toMap(), |
|||
where: 'id = ?', |
|||
whereArgs: [role.id], |
|||
); |
|||
} |
|||
|
|||
Future<int> deleteRole(int? id) async { |
|||
final db = await database; |
|||
return await db.delete( |
|||
'roles', |
|||
where: 'id = ?', |
|||
whereArgs: [id], |
|||
); |
|||
} |
|||
|
|||
// ========== GESTION DES PERMISSIONS ========== |
|||
|
|||
Future<List<Permission>> getAllPermissions() async { |
|||
final db = await database; |
|||
final result = await db.query('permissions', orderBy: 'name ASC'); |
|||
return result.map((e) => Permission.fromMap(e)).toList(); |
|||
} |
|||
|
|||
Future<List<Permission>> getPermissionsForRole(int roleId) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT p.id, p.name |
|||
FROM permissions p |
|||
JOIN role_permissions rp ON p.id = rp.permission_id |
|||
WHERE rp.role_id = ? |
|||
ORDER BY p.name ASC |
|||
''', [roleId]); |
|||
|
|||
return result.map((map) => Permission.fromMap(map)).toList(); |
|||
} |
|||
|
|||
Future<List<Permission>> getPermissionsForUser(String username) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT DISTINCT p.id, p.name |
|||
FROM permissions p |
|||
JOIN role_permissions rp ON p.id = rp.permission_id |
|||
JOIN roles r ON rp.role_id = r.id |
|||
JOIN users u ON u.role_id = r.id |
|||
WHERE u.username = ? |
|||
ORDER BY p.name ASC |
|||
''', [username]); |
|||
|
|||
return result.map((map) => Permission.fromMap(map)).toList(); |
|||
} |
|||
|
|||
Future<void> assignPermission(int roleId, int permissionId) async { |
|||
final db = await database; |
|||
await db.insert('role_permissions', { |
|||
'role_id': roleId, |
|||
'permission_id': permissionId, |
|||
}, conflictAlgorithm: ConflictAlgorithm.ignore); |
|||
} |
|||
|
|||
Future<void> removePermission(int roleId, int permissionId) async { |
|||
final db = await database; |
|||
await db.delete( |
|||
'role_permissions', |
|||
where: 'role_id = ? AND permission_id = ?', |
|||
whereArgs: [roleId, permissionId], |
|||
); |
|||
} |
|||
|
|||
// ========== MÉTHODES UTILITAIRES POUR LA SÉCURITÉ ========== |
|||
|
|||
Future<bool> isSuperAdmin(String username) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT COUNT(*) as count |
|||
FROM users u |
|||
INNER JOIN roles r ON u.role_id = r.id |
|||
WHERE u.username = ? AND r.designation = 'Super Admin' |
|||
''', [username]); |
|||
|
|||
return (result.first['count'] as int) > 0; |
|||
} |
|||
|
|||
Future<void> changePassword(String username, String oldPassword, String newPassword) async { |
|||
final db = await database; |
|||
|
|||
// Vérifier l'ancien mot de passe |
|||
final isValidOldPassword = await verifyUser(username, oldPassword); |
|||
if (!isValidOldPassword) { |
|||
throw Exception('Ancien mot de passe incorrect'); |
|||
} |
|||
|
|||
// Changer le mot de passe |
|||
await db.update( |
|||
'users', |
|||
{'password': newPassword}, |
|||
where: 'username = ?', |
|||
whereArgs: [username], |
|||
); |
|||
} |
|||
|
|||
// ========== UTILITAIRES ========== |
|||
|
|||
Future<void> close() async { |
|||
if (_database.isOpen) { |
|||
await _database.close(); |
|||
} |
|||
} |
|||
|
|||
Future<bool> hasPermission(String username, String permissionName) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT COUNT(*) as count |
|||
FROM permissions p |
|||
JOIN role_permissions rp ON p.id = rp.permission_id |
|||
JOIN roles r ON rp.role_id = r.id |
|||
JOIN users u ON u.role_id = r.id |
|||
WHERE u.username = ? AND p.name = ? |
|||
''', [username, permissionName]); |
|||
|
|||
return (result.first['count'] as int) > 0; |
|||
} |
|||
|
|||
// ========== MÉTHODE DE DEBUG ========== |
|||
|
|||
Future<void> printDatabaseInfo() async { |
|||
final db = await database; |
|||
|
|||
print("=== INFORMATIONS DE LA BASE DE DONNÉES ==="); |
|||
|
|||
// Compter les utilisateurs |
|||
final userCount = await getUserCount(); |
|||
print("Nombre d'utilisateurs: $userCount"); |
|||
|
|||
// Lister tous les utilisateurs |
|||
final users = await getAllUsers(); |
|||
print("Utilisateurs:"); |
|||
for (var user in users) { |
|||
print(" - ${user.username} (${user.name} ) - Email: ${user.email}"); |
|||
} |
|||
|
|||
// Lister tous les rôles |
|||
final roles = await getRoles(); |
|||
print("Rôles:"); |
|||
for (var role in roles) { |
|||
print(" - ${role.designation} (ID: ${role.id})"); |
|||
} |
|||
|
|||
// Lister toutes les permissions |
|||
final permissions = await getAllPermissions(); |
|||
print("Permissions:"); |
|||
for (var permission in permissions) { |
|||
print(" - ${permission.name} (ID: ${permission.id})"); |
|||
} |
|||
|
|||
print("========================================="); |
|||
} |
|||
} |
|||
@ -1,147 +0,0 @@ |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:path/path.dart'; |
|||
import 'package:path_provider/path_provider.dart'; |
|||
import 'package:sqflite/sqflite.dart'; |
|||
import 'package:sqflite_common_ffi/sqflite_ffi.dart' as sqflite_ffi; |
|||
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; |
|||
import 'package:youmazgestion/Models/users.dart'; |
|||
import 'dart:io'; |
|||
|
|||
class AuthDatabase { |
|||
static final AuthDatabase instance = AuthDatabase._init(); |
|||
late Database _database; |
|||
|
|||
AuthDatabase._init() { |
|||
sqflite_ffi.sqfliteFfiInit(); |
|||
} |
|||
|
|||
Future<void> initDatabase() async { |
|||
_database = await _initDB('usersDb.db'); |
|||
await _createDB(_database, 1); |
|||
} |
|||
|
|||
Future<Database> get database async { |
|||
if (_database.isOpen) return _database; |
|||
|
|||
_database = await _initDB('usersDb.db'); |
|||
return _database; |
|||
} |
|||
|
|||
Future<Database> _initDB(String filePath) async { |
|||
// Obtenez le répertoire de stockage local de l'application |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, filePath); |
|||
|
|||
// Vérifiez si le fichier de base de données existe déjà dans le répertoire de stockage local |
|||
bool dbExists = await File(path).exists(); |
|||
if (!dbExists) { |
|||
// Si le fichier n'existe pas, copiez-le depuis le dossier assets/database |
|||
ByteData data = await rootBundle.load('assets/database/$filePath'); |
|||
List<int> bytes = |
|||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); |
|||
await File(path).writeAsBytes(bytes); |
|||
} |
|||
|
|||
// Ouvrez la base de données |
|||
return await databaseFactoryFfi.openDatabase(path); |
|||
} |
|||
|
|||
Future<void> _createDB(Database db, int version) async { |
|||
final resultUsers = await db.rawQuery( |
|||
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'"); |
|||
|
|||
if (resultUsers.isEmpty) { |
|||
await db.execute(''' |
|||
CREATE TABLE users ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT, |
|||
lastname TEXT, |
|||
email TEXT, |
|||
password TEXT, |
|||
username TEXT, |
|||
role TEXT |
|||
) |
|||
'''); |
|||
} |
|||
} |
|||
|
|||
Future<int> createUser(Users user) async { |
|||
final db = await database; |
|||
return await db.insert('users', user.toMap()); |
|||
} |
|||
|
|||
Future<int> deleteUser(int id) async { |
|||
final db = await database; |
|||
return await db.delete('users', where: 'id = ?', whereArgs: [id]); |
|||
} |
|||
|
|||
Future<int> updateUser(Users user) async { |
|||
final db = await database; |
|||
return await db |
|||
.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]); |
|||
} |
|||
|
|||
Future<int> getUserCount() async { |
|||
final db = await database; |
|||
List<Map<String, dynamic>> x = |
|||
await db.rawQuery('SELECT COUNT (*) from users'); |
|||
int result = Sqflite.firstIntValue(x)!; |
|||
return result; |
|||
} |
|||
|
|||
// verify username and password existe |
|||
Future<bool> verifyUser(String username, String password) async { |
|||
final db = await database; |
|||
List<Map<String, dynamic>> x = await db.rawQuery( |
|||
'SELECT COUNT (*) from users WHERE username = ? AND password = ?', |
|||
[username, password]); |
|||
int result = Sqflite.firstIntValue(x)!; |
|||
if (result == 1) { |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
//recuperer un user grace a son username |
|||
Future<Users> getUser(String username) async { |
|||
try { |
|||
final db = await database; |
|||
List<Map<String, dynamic>> x = await db |
|||
.rawQuery('SELECT * from users WHERE username = ?', [username]); |
|||
print(x.first); |
|||
Users user = Users.fromMap(x.first); |
|||
print(user); |
|||
return user; |
|||
} catch (e) { |
|||
print(e); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
Future<Map<String, String>?> getUserCredentials(String username, String password) async { |
|||
final db = await database; |
|||
|
|||
List<Map<String, dynamic>> result = await db.rawQuery( |
|||
'SELECT username, role FROM users WHERE username = ? AND password = ?', |
|||
[username, password], |
|||
); |
|||
|
|||
if (result.isNotEmpty) { |
|||
print('username '+result[0]['username']); |
|||
return { |
|||
'username': result[0]['username'], |
|||
'role': result[0]['role'], |
|||
}; |
|||
} else { |
|||
return null; // Aucun utilisateur trouvé |
|||
} |
|||
} |
|||
|
|||
Future<List<Users>> getAllUsers() async { |
|||
final db = await database; |
|||
const orderBy = 'id ASC'; |
|||
final result = await db.query('users', orderBy: orderBy); |
|||
return result.map((json) => Users.fromMap(json)).toList(); |
|||
} |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:youmazgestion/Services/app_database.dart'; |
|||
import 'package:youmazgestion/Models/role.dart'; |
|||
|
|||
class HandleUserRole extends StatefulWidget { |
|||
const HandleUserRole({super.key}); |
|||
|
|||
@override |
|||
State<HandleUserRole> createState() => _HandleUserRoleState(); |
|||
} |
|||
|
|||
class _HandleUserRoleState extends State<HandleUserRole> { |
|||
final db = AppDatabase.instance; |
|||
|
|||
List<Map<String, dynamic>> roles = []; |
|||
Map<String, bool> permissionsMap = {}; |
|||
|
|||
int? selectedRoleId; |
|||
final TextEditingController _roleController = TextEditingController(); |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_initData(); |
|||
} |
|||
|
|||
Future<void> _initData() async { |
|||
final roleList = await db.database.then((db) => db.query('roles')); |
|||
final perms = await db.getAllPermissions(); |
|||
|
|||
setState(() { |
|||
roles = roleList; |
|||
for (var perm in perms) { |
|||
permissionsMap[perm.name] = false; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
Future<void> _addRole() async { |
|||
String designation = _roleController.text.trim(); |
|||
if (designation.isEmpty) return; |
|||
|
|||
await db.createRole(Role(designation: designation)); |
|||
_roleController.clear(); |
|||
await _initData(); |
|||
} |
|||
|
|||
Future<void> _loadRolePermissions(int roleId) async { |
|||
final rolePerms = await db.getPermissionsForRole(roleId); |
|||
final allPerms = await db.getAllPermissions(); |
|||
|
|||
setState(() { |
|||
selectedRoleId = roleId; |
|||
permissionsMap = { |
|||
for (var perm in allPerms) |
|||
perm.name: rolePerms.any((rp) => rp.name == perm.name) |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
Future<void> _onPermissionToggle(String permission, bool enabled) async { |
|||
if (selectedRoleId == null) return; |
|||
|
|||
final allPerms = await db.getAllPermissions(); |
|||
final perm = allPerms.firstWhere((p) => p.name == permission); |
|||
|
|||
if (enabled) { |
|||
await db.assignPermission(selectedRoleId!, perm.id!); |
|||
} else { |
|||
await db.removePermission(selectedRoleId!, perm.id!); |
|||
} |
|||
|
|||
setState(() { |
|||
permissionsMap[permission] = enabled; |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: const CustomAppBar(title: "Gestion des rôles"), |
|||
body: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
children: [ |
|||
// ✅ Champ pour saisir un nouveau rôle |
|||
Row( |
|||
children: [ |
|||
Expanded( |
|||
child: TextField( |
|||
controller: _roleController, |
|||
decoration: const InputDecoration( |
|||
labelText: 'Nouveau rôle', |
|||
border: OutlineInputBorder(), |
|||
), |
|||
), |
|||
), |
|||
const SizedBox(width: 10), |
|||
ElevatedButton( |
|||
onPressed: _addRole, |
|||
child: const Text('Créer'), |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 20), |
|||
|
|||
// 🔽 Sélection d'un rôle |
|||
DropdownButton<int>( |
|||
isExpanded: true, |
|||
hint: const Text("Choisir un rôle"), |
|||
value: selectedRoleId, |
|||
items: roles.map((role) { |
|||
return DropdownMenuItem<int>( |
|||
value: role['id'] as int, |
|||
child: Text(role['designation'] ?? ''), |
|||
); |
|||
}).toList(), |
|||
onChanged: (value) { |
|||
if (value != null) _loadRolePermissions(value); |
|||
}, |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
// ✅ Permissions associées |
|||
if (selectedRoleId != null) |
|||
Expanded( |
|||
child: ListView( |
|||
children: permissionsMap.entries.map((entry) { |
|||
return CheckboxListTile( |
|||
title: Text(entry.key), |
|||
value: entry.value, |
|||
onChanged: (bool? value) { |
|||
_onPermissionToggle(entry.key, value ?? false); |
|||
}, |
|||
); |
|||
}).toList(), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,449 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'dart:io'; |
|||
import 'package:quantity_input/quantity_input.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
|
|||
class ProductCard extends StatefulWidget { |
|||
final Product product; |
|||
final void Function(Product, int) onAddToCart; |
|||
|
|||
const ProductCard({ |
|||
Key? key, |
|||
required this.product, |
|||
required this.onAddToCart, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
State<ProductCard> createState() => _ProductCardState(); |
|||
} |
|||
|
|||
class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin { |
|||
int selectedQuantity = 1; |
|||
late AnimationController _scaleController; |
|||
late AnimationController _fadeController; |
|||
late Animation<double> _scaleAnimation; |
|||
late Animation<double> _fadeAnimation; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
|
|||
// Animations pour les interactions |
|||
_scaleController = AnimationController( |
|||
duration: const Duration(milliseconds: 200), |
|||
vsync: this, |
|||
); |
|||
_fadeController = AnimationController( |
|||
duration: const Duration(milliseconds: 300), |
|||
vsync: this, |
|||
)..forward(); |
|||
|
|||
_scaleAnimation = Tween<double>( |
|||
begin: 1.0, |
|||
end: 0.95, |
|||
).animate(CurvedAnimation( |
|||
parent: _scaleController, |
|||
curve: Curves.easeInOut, |
|||
)); |
|||
|
|||
_fadeAnimation = Tween<double>( |
|||
begin: 0.0, |
|||
end: 1.0, |
|||
).animate(CurvedAnimation( |
|||
parent: _fadeController, |
|||
curve: Curves.easeOut, |
|||
)); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_scaleController.dispose(); |
|||
_fadeController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
void _onTapDown(TapDownDetails details) { |
|||
_scaleController.forward(); |
|||
} |
|||
|
|||
void _onTapUp(TapUpDetails details) { |
|||
_scaleController.reverse(); |
|||
} |
|||
|
|||
void _onTapCancel() { |
|||
_scaleController.reverse(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return FadeTransition( |
|||
opacity: _fadeAnimation, |
|||
child: AnimatedBuilder( |
|||
animation: _scaleAnimation, |
|||
builder: (context, child) { |
|||
return Transform.scale( |
|||
scale: _scaleAnimation.value, |
|||
child: Container( |
|||
margin: const EdgeInsets.all(8), |
|||
height: 280, |
|||
child: Material( |
|||
elevation: 8, |
|||
shadowColor: Colors.black.withOpacity(0.2), |
|||
borderRadius: BorderRadius.circular(20), |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(20), |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
colors: [ |
|||
Colors.white, |
|||
Colors.grey.shade50, |
|||
], |
|||
), |
|||
), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(20), |
|||
child: Stack( |
|||
children: [ |
|||
Positioned.fill( |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade100, |
|||
borderRadius: BorderRadius.circular(20), |
|||
), |
|||
child: widget.product.image != null |
|||
? Image.file( |
|||
File(widget.product.image), |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return _buildPlaceholderImage(); |
|||
}, |
|||
) |
|||
: _buildPlaceholderImage(), |
|||
), |
|||
), |
|||
|
|||
Positioned.fill( |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(20), |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topCenter, |
|||
end: Alignment.bottomCenter, |
|||
colors: [ |
|||
Colors.transparent, |
|||
Colors.transparent, |
|||
Colors.black.withOpacity(0.3), |
|||
Colors.black.withOpacity(0.7), |
|||
], |
|||
stops: const [0.0, 0.4, 0.7, 1.0], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
|
|||
if (widget.product.isStockDefined()) |
|||
Positioned( |
|||
top: 12, |
|||
right: 12, |
|||
child: Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 8, |
|||
vertical: 4, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: Colors.green.withOpacity(0.9), |
|||
borderRadius: BorderRadius.circular(12), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.green.withOpacity(0.3), |
|||
blurRadius: 4, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
const Icon( |
|||
Icons.check_circle, |
|||
color: Colors.white, |
|||
size: 12, |
|||
), |
|||
const SizedBox(width: 4), |
|||
const Text( |
|||
'En stock', |
|||
style: TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 10, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
|
|||
Positioned( |
|||
left: 0, |
|||
right: 0, |
|||
bottom: 0, |
|||
child: Container( |
|||
padding: const EdgeInsets.all(16), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Container( |
|||
width: double.infinity, |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 12, |
|||
vertical: 8, |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
widget.product.name, |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 14, |
|||
color: Colors.white, |
|||
shadows: [ |
|||
Shadow( |
|||
offset: Offset(1, 1), |
|||
blurRadius: 3, |
|||
color: Colors.black54, |
|||
), |
|||
], |
|||
), |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
const SizedBox(height: 4), |
|||
Text( |
|||
'${widget.product.price.toStringAsFixed(2)} FCFA', |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 13, |
|||
color: Colors.white, |
|||
shadows: [ |
|||
Shadow( |
|||
offset: Offset(1, 1), |
|||
blurRadius: 3, |
|||
color: Colors.black54, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 12), |
|||
|
|||
Row( |
|||
children: [ |
|||
Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.white.withOpacity(0.95), |
|||
borderRadius: BorderRadius.circular(20), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.1), |
|||
blurRadius: 4, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
_buildQuantityButton( |
|||
icon: Icons.remove, |
|||
onPressed: selectedQuantity > 1 |
|||
? () { |
|||
setState(() { |
|||
selectedQuantity--; |
|||
}); |
|||
} |
|||
: null, |
|||
), |
|||
Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 8, |
|||
vertical: 4, |
|||
), |
|||
child: Text( |
|||
'$selectedQuantity', |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 14, |
|||
), |
|||
), |
|||
), |
|||
_buildQuantityButton( |
|||
icon: Icons.add, |
|||
onPressed: selectedQuantity < 100 |
|||
? () { |
|||
setState(() { |
|||
selectedQuantity++; |
|||
}); |
|||
} |
|||
: null, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const SizedBox(width: 8), |
|||
|
|||
Expanded( |
|||
child: GestureDetector( |
|||
onTapDown: _onTapDown, |
|||
onTapUp: _onTapUp, |
|||
onTapCancel: _onTapCancel, |
|||
onTap: () { |
|||
widget.onAddToCart(widget.product, selectedQuantity); |
|||
|
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar( |
|||
content: Row( |
|||
children: [ |
|||
const Icon( |
|||
Icons.shopping_cart, |
|||
color: Colors.white, |
|||
), |
|||
const SizedBox(width: 8), |
|||
Expanded( |
|||
child: Text( |
|||
'${widget.product.name} (x$selectedQuantity) ajouté au panier', |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
backgroundColor: Colors.green, |
|||
duration: const Duration(seconds: 1), |
|||
behavior: SnackBarBehavior.floating, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(10), |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
child: Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
vertical: 10, |
|||
horizontal: 12, |
|||
), |
|||
decoration: BoxDecoration( |
|||
gradient: const LinearGradient( |
|||
colors: [ |
|||
Color.fromARGB(255, 4, 54, 95), |
|||
Color.fromARGB(255, 6, 80, 140), |
|||
], |
|||
), |
|||
borderRadius: BorderRadius.circular(20), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3), |
|||
blurRadius: 6, |
|||
offset: const Offset(0, 3), |
|||
), |
|||
], |
|||
), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
const Icon( |
|||
Icons.add_shopping_cart, |
|||
color: Colors.white, |
|||
size: 16, |
|||
), |
|||
const SizedBox(width: 6), |
|||
const Flexible( |
|||
child: Text( |
|||
'Ajouter', |
|||
style: TextStyle( |
|||
color: Colors.white, |
|||
fontWeight: FontWeight.bold, |
|||
fontSize: 12, |
|||
), |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildPlaceholderImage() { |
|||
return Container( |
|||
width: double.infinity, |
|||
height: double.infinity, |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade200, |
|||
borderRadius: BorderRadius.circular(20), |
|||
), |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Icon( |
|||
Icons.image_outlined, |
|||
size: 40, |
|||
color: Colors.grey.shade400, |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'Image non disponible', |
|||
style: TextStyle( |
|||
color: Colors.grey.shade500, |
|||
fontSize: 12, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildQuantityButton({ |
|||
required IconData icon, |
|||
required VoidCallback? onPressed, |
|||
}) { |
|||
return Material( |
|||
color: Colors.transparent, |
|||
child: InkWell( |
|||
onTap: onPressed, |
|||
borderRadius: BorderRadius.circular(15), |
|||
child: Container( |
|||
padding: const EdgeInsets.all(6), |
|||
child: Icon( |
|||
icon, |
|||
size: 16, |
|||
color: onPressed != null ? const Color.fromARGB(255, 4, 54, 95) : Colors.grey, |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue