9 changed files with 919 additions and 188 deletions
@ -0,0 +1,94 @@ |
|||
// Models/menu.dart |
|||
class Menu { |
|||
final int? id; |
|||
final String title; |
|||
final String icon; |
|||
final String route; |
|||
final int orderIndex; |
|||
final bool isActive; |
|||
final int? parentId; |
|||
|
|||
Menu({ |
|||
this.id, |
|||
required this.title, |
|||
required this.icon, |
|||
required this.route, |
|||
this.orderIndex = 0, |
|||
this.isActive = true, |
|||
this.parentId, |
|||
}); |
|||
|
|||
Map<String, dynamic> toMap() { |
|||
return { |
|||
'id': id, |
|||
'title': title, |
|||
'icon': icon, |
|||
'route': route, |
|||
'order_index': orderIndex, |
|||
'is_active': isActive ? 1 : 0, |
|||
'parent_id': parentId, |
|||
}; |
|||
} |
|||
|
|||
factory Menu.fromMap(Map<String, dynamic> map) { |
|||
return Menu( |
|||
id: map['id']?.toInt(), |
|||
title: map['title'] ?? '', |
|||
icon: map['icon'] ?? '', |
|||
route: map['route'] ?? '', |
|||
orderIndex: map['order_index']?.toInt() ?? 0, |
|||
isActive: (map['is_active'] ?? 1) == 1, |
|||
parentId: map['parent_id']?.toInt(), |
|||
); |
|||
} |
|||
|
|||
Menu copyWith({ |
|||
int? id, |
|||
String? title, |
|||
String? icon, |
|||
String? route, |
|||
int? orderIndex, |
|||
bool? isActive, |
|||
int? parentId, |
|||
}) { |
|||
return Menu( |
|||
id: id ?? this.id, |
|||
title: title ?? this.title, |
|||
icon: icon ?? this.icon, |
|||
route: route ?? this.route, |
|||
orderIndex: orderIndex ?? this.orderIndex, |
|||
isActive: isActive ?? this.isActive, |
|||
parentId: parentId ?? this.parentId, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
String toString() { |
|||
return 'Menu(id: $id, title: $title, icon: $icon, route: $route, orderIndex: $orderIndex, isActive: $isActive, parentId: $parentId)'; |
|||
} |
|||
|
|||
@override |
|||
bool operator ==(Object other) { |
|||
if (identical(this, other)) return true; |
|||
|
|||
return other is Menu && |
|||
other.id == id && |
|||
other.title == title && |
|||
other.icon == icon && |
|||
other.route == route && |
|||
other.orderIndex == orderIndex && |
|||
other.isActive == isActive && |
|||
other.parentId == parentId; |
|||
} |
|||
|
|||
@override |
|||
int get hashCode { |
|||
return id.hashCode ^ |
|||
title.hashCode ^ |
|||
icon.hashCode ^ |
|||
route.hashCode ^ |
|||
orderIndex.hashCode ^ |
|||
isActive.hashCode ^ |
|||
parentId.hashCode; |
|||
} |
|||
} |
|||
@ -0,0 +1,215 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:youmazgestion/Models/Permission.dart'; |
|||
import 'package:youmazgestion/Services/app_database.dart'; |
|||
import 'package:youmazgestion/Models/role.dart'; |
|||
import 'package:youmazgestion/Views/RolePermissionPage.dart'; |
|||
|
|||
class RoleListPage extends StatefulWidget { |
|||
const RoleListPage({super.key}); |
|||
|
|||
@override |
|||
State<RoleListPage> createState() => _RoleListPageState(); |
|||
} |
|||
|
|||
class _RoleListPageState extends State<RoleListPage> { |
|||
final db = AppDatabase.instance; |
|||
final TextEditingController _roleController = TextEditingController(); |
|||
List<Role> roles = []; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadRoles(); |
|||
} |
|||
|
|||
Future<void> _loadRoles() async { |
|||
final roleList = await db.getRoles(); |
|||
setState(() { |
|||
roles = roleList; |
|||
}); |
|||
} |
|||
|
|||
Future<void> _addRole() async { |
|||
String designation = _roleController.text.trim(); |
|||
if (designation.isEmpty) return; |
|||
|
|||
await db.createRole(Role(designation: designation)); |
|||
_roleController.clear(); |
|||
await _loadRoles(); |
|||
} |
|||
|
|||
Future<void> _deleteRole(int roleId) async { |
|||
await db.deleteRole(roleId); |
|||
await _loadRoles(); |
|||
} |
|||
|
|||
@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: [ |
|||
// Section d'ajout de rôle |
|||
Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(10), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
children: [ |
|||
Text( |
|||
'Ajouter un nouveau rôle', |
|||
style: Theme.of(context).textTheme.titleMedium?.copyWith( |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
const SizedBox(height: 10), |
|||
Row( |
|||
children: [ |
|||
Expanded( |
|||
child: TextField( |
|||
controller: _roleController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Nom du rôle', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
contentPadding: const EdgeInsets.symmetric( |
|||
horizontal: 12, vertical: 12), |
|||
), |
|||
), |
|||
), |
|||
const SizedBox(width: 10), |
|||
ElevatedButton( |
|||
onPressed: _addRole, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
// padding: const EdgeInsets.symmetric( |
|||
// horizontal: 20, vertical: 15), |
|||
), |
|||
|
|||
), |
|||
child: const Text('Ajouter'), |
|||
) |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
const SizedBox(height: 20), |
|||
// Liste des rôles existants |
|||
Expanded( |
|||
child: Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(10), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Liste des rôles', |
|||
style: Theme.of(context).textTheme.titleMedium?.copyWith( |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
const SizedBox(height: 10), |
|||
Expanded( |
|||
child: roles.isEmpty |
|||
? const Center( |
|||
child: Text('Aucun rôle créé'), |
|||
) |
|||
: ListView.builder( |
|||
itemCount: roles.length, |
|||
itemBuilder: (context, index) { |
|||
final role = roles[index]; |
|||
return Card( |
|||
margin: const EdgeInsets.only(bottom: 8), |
|||
elevation: 2, |
|||
child: ListTile( |
|||
title: Text(role.designation), |
|||
trailing: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
IconButton( |
|||
icon: const Icon(Icons.edit, |
|||
color: Colors.blue), |
|||
onPressed: () { |
|||
_navigateToRolePermissions( |
|||
context, role); |
|||
}, |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.delete, |
|||
color: Colors.red), |
|||
onPressed: () { |
|||
_showDeleteDialog(role); |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
onTap: () { |
|||
_navigateToRolePermissions( |
|||
context, role); |
|||
}, |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _navigateToRolePermissions(BuildContext context, Role role) { |
|||
Navigator.push( |
|||
context, |
|||
MaterialPageRoute( |
|||
builder: (context) => RolePermissionsPage(role: role), |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showDeleteDialog(Role role) { |
|||
showDialog( |
|||
context: context, |
|||
builder: (BuildContext context) { |
|||
return AlertDialog( |
|||
title: const Text('Confirmer la suppression'), |
|||
content: Text( |
|||
'Êtes-vous sûr de vouloir supprimer le rôle "${role.designation}" ?'), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
child: const Text('Annuler'), |
|||
), |
|||
TextButton( |
|||
onPressed: () async { |
|||
Navigator.of(context).pop(); |
|||
await _deleteRole(role.id!); |
|||
}, |
|||
child: const Text('Supprimer', style: TextStyle(color: Colors.red)), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:youmazgestion/Models/Permission.dart'; |
|||
import 'package:youmazgestion/Services/app_database.dart'; |
|||
import 'package:youmazgestion/Models/role.dart'; |
|||
|
|||
class RolePermissionsPage extends StatefulWidget { |
|||
final Role role; |
|||
|
|||
const RolePermissionsPage({super.key, required this.role}); |
|||
|
|||
@override |
|||
State<RolePermissionsPage> createState() => _RolePermissionsPageState(); |
|||
} |
|||
|
|||
class _RolePermissionsPageState extends State<RolePermissionsPage> { |
|||
final db = AppDatabase.instance; |
|||
List<Permission> permissions = []; |
|||
List<Map<String, dynamic>> menus = []; |
|||
Map<int, Map<String, bool>> menuPermissionsMap = {}; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_initData(); |
|||
} |
|||
|
|||
Future<void> _initData() async { |
|||
final perms = await db.getAllPermissions(); |
|||
final menuList = await db.database.then((db) => db.query('menu')); |
|||
|
|||
Map<int, Map<String, bool>> tempMenuPermissionsMap = {}; |
|||
|
|||
for (var menu in menuList) { |
|||
final menuId = menu['id'] as int; |
|||
final menuPerms = await db.getPermissionsForRoleAndMenu( |
|||
widget.role.id!, menuId); |
|||
|
|||
tempMenuPermissionsMap[menuId] = { |
|||
for (var perm in perms) |
|||
perm.name: menuPerms.any((mp) => mp.name == perm.name) |
|||
}; |
|||
} |
|||
|
|||
setState(() { |
|||
permissions = perms; |
|||
menus = menuList; |
|||
menuPermissionsMap = tempMenuPermissionsMap; |
|||
}); |
|||
} |
|||
|
|||
Future<void> _onPermissionToggle( |
|||
int menuId, String permission, bool enabled) async { |
|||
final perm = permissions.firstWhere((p) => p.name == permission); |
|||
|
|||
if (enabled) { |
|||
await db.assignRoleMenuPermission( |
|||
widget.role.id!, menuId, perm.id!); |
|||
} else { |
|||
await db.removeRoleMenuPermission( |
|||
widget.role.id!, menuId, perm.id!); |
|||
} |
|||
|
|||
setState(() { |
|||
menuPermissionsMap[menuId]![permission] = enabled; |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: CustomAppBar( |
|||
title: "Permissions - ${widget.role.designation}", |
|||
// showBackButton: true, |
|||
), |
|||
body: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Gestion des permissions pour le rôle: ${widget.role.designation}', |
|||
style: Theme.of(context).textTheme.titleLarge?.copyWith( |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
const SizedBox(height: 10), |
|||
const Text( |
|||
'Sélectionnez les permissions pour chaque menu:', |
|||
style: TextStyle(fontSize: 14, color: Colors.grey), |
|||
), |
|||
const SizedBox(height: 20), |
|||
if (permissions.isNotEmpty && menus.isNotEmpty) |
|||
Expanded( |
|||
child: ListView.builder( |
|||
itemCount: menus.length, |
|||
itemBuilder: (context, index) { |
|||
final menu = menus[index]; |
|||
final menuId = menu['id'] as int; |
|||
final menuName = menu['name'] as String; |
|||
|
|||
return Card( |
|||
margin: const EdgeInsets.only(bottom: 15), |
|||
elevation: 3, |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(12.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
menuName, |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, fontSize: 16), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Wrap( |
|||
spacing: 10, |
|||
runSpacing: 10, |
|||
children: permissions.map((perm) { |
|||
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false; |
|||
return FilterChip( |
|||
label: perm.name, |
|||
selected: isChecked, |
|||
onSelected: (bool value) { |
|||
_onPermissionToggle(menuId, perm.name, value); |
|||
}, |
|||
); |
|||
}).toList(), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
) |
|||
else |
|||
const Expanded( |
|||
child: Center( |
|||
child: CircularProgressIndicator(), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
class FilterChip extends StatelessWidget { |
|||
final String label; |
|||
final bool selected; |
|||
final ValueChanged<bool> onSelected; |
|||
|
|||
const FilterChip({ |
|||
super.key, |
|||
required this.label, |
|||
required this.selected, |
|||
required this.onSelected, |
|||
}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return ChoiceChip( |
|||
label: Text(label), |
|||
selected: selected, |
|||
onSelected: onSelected, |
|||
selectedColor: Colors.blue, |
|||
labelStyle: TextStyle( |
|||
color: selected ? Colors.white : Colors.black, |
|||
), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(20), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue