Compare commits
2 Commits
master
...
31052025_0
| Author | SHA1 | Date |
|---|---|---|
|
|
c8febdaff9 | 6 months ago |
|
|
57ea91b3d7 | 6 months ago |
36 changed files with 3959 additions and 2071 deletions
@ -0,0 +1,207 @@ |
|||
// Ajoutez cette importation en haut du fichier |
|||
import 'dart:ui'; |
|||
|
|||
import 'package:flutter/material.dart'; |
|||
import 'package:mobile_scanner/mobile_scanner.dart'; |
|||
|
|||
// Créez une nouvelle classe pour la page de scan QR |
|||
class ScanQRPage extends StatefulWidget { |
|||
const ScanQRPage({super.key}); |
|||
|
|||
@override |
|||
State<ScanQRPage> createState() => _ScanQRPageState(); |
|||
} |
|||
|
|||
class _ScanQRPageState extends State<ScanQRPage> { |
|||
MobileScannerController cameraController = MobileScannerController(); |
|||
bool _isScanComplete = false; |
|||
String? _scannedData; |
|||
|
|||
@override |
|||
void dispose() { |
|||
cameraController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: AppBar( |
|||
title: const Text('Scanner QR Code'), |
|||
actions: [ |
|||
IconButton( |
|||
color: Colors.white, |
|||
icon: ValueListenableBuilder( |
|||
valueListenable: cameraController.torchState, |
|||
builder: (context, state, child) { |
|||
switch (state) { |
|||
case TorchState.off: |
|||
return const Icon(Icons.flash_off, color: Colors.grey); |
|||
case TorchState.on: |
|||
return const Icon(Icons.flash_on, color: Colors.yellow); |
|||
} |
|||
}, |
|||
), |
|||
iconSize: 32.0, |
|||
onPressed: () => cameraController.toggleTorch(), |
|||
), |
|||
IconButton( |
|||
color: Colors.white, |
|||
icon: ValueListenableBuilder( |
|||
valueListenable: cameraController.cameraFacingState, |
|||
builder: (context, state, child) { |
|||
switch (state) { |
|||
case CameraFacing.front: |
|||
return const Icon(Icons.camera_front); |
|||
case CameraFacing.back: |
|||
return const Icon(Icons.camera_rear); |
|||
} |
|||
}, |
|||
), |
|||
iconSize: 32.0, |
|||
onPressed: () => cameraController.switchCamera(), |
|||
), |
|||
], |
|||
), |
|||
body: Stack( |
|||
children: [ |
|||
MobileScanner( |
|||
controller: cameraController, |
|||
onDetect: (capture) { |
|||
final List<Barcode> barcodes = capture.barcodes; |
|||
for (final barcode in barcodes) { |
|||
if (!_isScanComplete && barcode.rawValue != null) { |
|||
_isScanComplete = true; |
|||
_scannedData = barcode.rawValue; |
|||
_showScanResult(context, _scannedData!); |
|||
} |
|||
} |
|||
}, |
|||
), |
|||
CustomPaint( |
|||
painter: QrScannerOverlay( |
|||
borderColor: Colors.blue.shade800, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showScanResult(BuildContext context, String data) { |
|||
showDialog( |
|||
context: context, |
|||
builder: (context) => AlertDialog( |
|||
title: const Text('Résultat du scan'), |
|||
content: Text(data), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () { |
|||
Navigator.pop(context); |
|||
setState(() { |
|||
_isScanComplete = false; |
|||
}); |
|||
}, |
|||
child: const Text('OK'), |
|||
), |
|||
], |
|||
), |
|||
).then((_) { |
|||
// Réinitialiser le scan après la fermeture du dialogue |
|||
setState(() { |
|||
_isScanComplete = false; |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// Widget personnalisé pour l'overlay du scanner |
|||
class QrScannerOverlay extends CustomPainter { |
|||
final Color borderColor; |
|||
|
|||
QrScannerOverlay({required this.borderColor}); |
|||
|
|||
@override |
|||
void paint(Canvas canvas, Size size) { |
|||
final double width = size.width; |
|||
final double height = size.height; |
|||
final double borderWidth = 2.0; |
|||
final double borderLength = 30.0; |
|||
final double areaSize = width * 0.7; |
|||
|
|||
// Dessiner un rectangle semi-transparent autour de la zone de scan |
|||
final Paint backgroundPaint = Paint() |
|||
..color = Colors.black.withOpacity(0.4); |
|||
canvas.drawRect(Rect.fromLTRB(0, 0, width, height), backgroundPaint); |
|||
|
|||
// Dessiner la zone de scan transparente |
|||
final Paint transparentPaint = Paint() |
|||
..color = Colors.transparent |
|||
..blendMode = BlendMode.clear; |
|||
final double areaLeft = (width - areaSize) / 2; |
|||
final double areaTop = (height - areaSize) / 2; |
|||
canvas.drawRect( |
|||
Rect.fromLTRB(areaLeft, areaTop, areaLeft + areaSize, areaTop + areaSize), |
|||
transparentPaint, |
|||
); |
|||
|
|||
// Dessiner les bordures de la zone de scan |
|||
final Paint borderPaint = Paint() |
|||
..color = borderColor |
|||
..strokeWidth = borderWidth |
|||
..style = PaintingStyle.stroke; |
|||
|
|||
// Coin supérieur gauche |
|||
canvas.drawLine( |
|||
Offset(areaLeft, areaTop), |
|||
Offset(areaLeft + borderLength, areaTop), |
|||
borderPaint, |
|||
); |
|||
canvas.drawLine( |
|||
Offset(areaLeft, areaTop), |
|||
Offset(areaLeft, areaTop + borderLength), |
|||
borderPaint, |
|||
); |
|||
|
|||
// Coin supérieur droit |
|||
canvas.drawLine( |
|||
Offset(areaLeft + areaSize - borderLength, areaTop), |
|||
Offset(areaLeft + areaSize, areaTop), |
|||
borderPaint, |
|||
); |
|||
canvas.drawLine( |
|||
Offset(areaLeft + areaSize, areaTop), |
|||
Offset(areaLeft + areaSize, areaTop + borderLength), |
|||
borderPaint, |
|||
); |
|||
|
|||
// Coin inférieur gauche |
|||
canvas.drawLine( |
|||
Offset(areaLeft, areaTop + areaSize - borderLength), |
|||
Offset(areaLeft, areaTop + areaSize), |
|||
borderPaint, |
|||
); |
|||
canvas.drawLine( |
|||
Offset(areaLeft, areaTop + areaSize), |
|||
Offset(areaLeft + borderLength, areaTop + areaSize), |
|||
borderPaint, |
|||
); |
|||
|
|||
// Coin inférieur droit |
|||
canvas.drawLine( |
|||
Offset(areaLeft + areaSize - borderLength, areaTop + areaSize), |
|||
Offset(areaLeft + areaSize, areaTop + areaSize), |
|||
borderPaint, |
|||
); |
|||
canvas.drawLine( |
|||
Offset(areaLeft + areaSize, areaTop + areaSize - borderLength), |
|||
Offset(areaLeft + areaSize, areaTop + areaSize), |
|||
borderPaint, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
bool shouldRepaint(covariant CustomPainter oldDelegate) { |
|||
return false; |
|||
} |
|||
} |
|||
@ -1,31 +1,117 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/controller/userController.dart'; |
|||
|
|||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { |
|||
final String title; |
|||
final Widget? subtitle; |
|||
final List<Widget>? actions; |
|||
final bool automaticallyImplyLeading; |
|||
final Color? backgroundColor; |
|||
|
|||
const CustomAppBar({ |
|||
final UserController userController = Get.put(UserController()); |
|||
|
|||
CustomAppBar({ |
|||
Key? key, |
|||
required this.title, |
|||
this.subtitle, |
|||
this.actions, |
|||
this.automaticallyImplyLeading = true, |
|||
this.backgroundColor, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0); |
|||
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return AppBar( |
|||
title: subtitle == null |
|||
? Text(title) |
|||
: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text(title, style: TextStyle(fontSize: 20)), |
|||
subtitle!, |
|||
return Container( |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
colors: [ |
|||
Colors.blue.shade900, |
|||
Colors.blue.shade800, |
|||
], |
|||
), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.blue.shade900.withOpacity(0.3), |
|||
offset: const Offset(0, 2), |
|||
blurRadius: 4, |
|||
), |
|||
], |
|||
), |
|||
child: AppBar( |
|||
backgroundColor: backgroundColor ?? Colors.transparent, |
|||
elevation: 0, |
|||
automaticallyImplyLeading: automaticallyImplyLeading, |
|||
centerTitle: false, |
|||
iconTheme: const IconThemeData( |
|||
color: Colors.white, |
|||
size: 24, |
|||
), |
|||
actions: actions, |
|||
title: subtitle == null |
|||
? Text( |
|||
title, |
|||
style: const TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
letterSpacing: 0.5, |
|||
), |
|||
) |
|||
: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Text( |
|||
title, |
|||
style: const TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
letterSpacing: 0.5, |
|||
), |
|||
), |
|||
const SizedBox(height: 2), |
|||
Obx(() => Text( |
|||
userController.role!='Super Admin'?'Point de vente: ${userController.pointDeVenteDesignation}':'', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.w400, |
|||
color: Colors.white.withOpacity(0.9), |
|||
letterSpacing: 0.3, |
|||
), |
|||
)), |
|||
if (subtitle != null) ...[ |
|||
const SizedBox(height: 2), |
|||
DefaultTextStyle( |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.white.withOpacity(0.8), |
|||
fontWeight: FontWeight.w400, |
|||
), |
|||
child: subtitle!, |
|||
), |
|||
], |
|||
], |
|||
), |
|||
flexibleSpace: Container( |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
colors: [ |
|||
Colors.blue.shade900, |
|||
Colors.blue.shade800, |
|||
], |
|||
), |
|||
// autres propriétés si besoin |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -1,680 +0,0 @@ |
|||
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 insertDefaultMenus(); |
|||
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) { |
|||
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) { |
|||
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(); |
|||
|
|||
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."); |
|||
} |
|||
|
|||
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."); |
|||
} |
|||
|
|||
if (!tableNames.contains('menu')) { |
|||
await db.execute(''' |
|||
CREATE TABLE menu ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL UNIQUE, |
|||
route TEXT NOT NULL UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'menu' créée."); |
|||
} |
|||
|
|||
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."); |
|||
} |
|||
|
|||
if (!tableNames.contains('menu_permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE menu_permissions ( |
|||
menu_id INTEGER, |
|||
permission_id INTEGER, |
|||
PRIMARY KEY (menu_id, permission_id), |
|||
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'menu_permissions' créée."); |
|||
} |
|||
|
|||
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."); |
|||
} |
|||
if (!tableNames.contains('role_menu_permissions')) { |
|||
await db.execute(''' |
|||
CREATE TABLE role_menu_permissions ( |
|||
role_id INTEGER, |
|||
menu_id INTEGER, |
|||
permission_id INTEGER, |
|||
PRIMARY KEY (role_id, menu_id, permission_id), |
|||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'role_menu_permissions' créée."); |
|||
} |
|||
|
|||
} |
|||
|
|||
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'}); |
|||
await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission |
|||
await db.insert('permissions', {'name': 'read'}); // Nouvelle permission |
|||
print("Permissions par défaut insérées"); |
|||
} else { |
|||
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas |
|||
final newPermissions = ['manage', 'read']; |
|||
for (var permission in newPermissions) { |
|||
final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]); |
|||
if (existingPermission.isEmpty) { |
|||
await db.insert('permissions', {'name': permission}); |
|||
print("Permission ajoutée: $permission"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> insertDefaultMenus() async { |
|||
final db = await database; |
|||
final existingMenus = await db.query('menu'); |
|||
|
|||
if (existingMenus.isEmpty) { |
|||
// Menus existants |
|||
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'}); |
|||
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}); |
|||
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}); |
|||
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}); |
|||
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}); |
|||
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'}); |
|||
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'}); |
|||
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'}); |
|||
await db.insert('menu', {'name': 'Historique', 'route': '/historique'}); |
|||
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'}); |
|||
|
|||
// Nouveaux menus ajoutés |
|||
await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}); |
|||
await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}); |
|||
|
|||
print("Menus par défaut insérés"); |
|||
} else { |
|||
// Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants |
|||
await _addMissingMenus(db); |
|||
} |
|||
} |
|||
|
|||
Future<void> _addMissingMenus(Database db) async { |
|||
final menusToAdd = [ |
|||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, |
|||
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, |
|||
]; |
|||
|
|||
for (var menu in menusToAdd) { |
|||
final existing = await db.query( |
|||
'menu', |
|||
where: 'route = ?', |
|||
whereArgs: [menu['route']], |
|||
); |
|||
|
|||
if (existing.isEmpty) { |
|||
await db.insert('menu', menu); |
|||
print("Menu ajouté: ${menu['name']}"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> insertDefaultRoles() async { |
|||
final db = await database; |
|||
final existingRoles = await db.query('roles'); |
|||
|
|||
if (existingRoles.isEmpty) { |
|||
int superAdminRoleId = await db.insert('roles', {'designation': 'Super Admin'}); |
|||
int adminRoleId = await db.insert('roles', {'designation': 'Admin'}); |
|||
int userRoleId = await db.insert('roles', {'designation': 'User'}); |
|||
|
|||
final permissions = await db.query('permissions'); |
|||
final menus = await db.query('menu'); |
|||
|
|||
// Assigner toutes les permissions à tous les menus pour le Super Admin |
|||
for (var menu in menus) { |
|||
for (var permission in permissions) { |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': superAdminRoleId, |
|||
'menu_id': menu['id'], |
|||
'permission_id': permission['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus |
|||
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId); |
|||
|
|||
print("Rôles par défaut créés et permissions assignées"); |
|||
} else { |
|||
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes |
|||
await _updateExistingRolePermissions(db); |
|||
} |
|||
} |
|||
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus |
|||
Future<void> _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async { |
|||
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']); |
|||
final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']); |
|||
|
|||
// Récupérer les IDs des nouveaux menus |
|||
final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']); |
|||
final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']); |
|||
|
|||
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) { |
|||
// Admin peut créer de nouvelles commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': adminRoleId, |
|||
'menu_id': nouvelleCommandeMenu.first['id'], |
|||
'permission_id': createPermission.first['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
|
|||
// User peut aussi créer de nouvelles commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': userRoleId, |
|||
'menu_id': nouvelleCommandeMenu.first['id'], |
|||
'permission_id': createPermission.first['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
|
|||
if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) { |
|||
// Admin peut gérer les commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': adminRoleId, |
|||
'menu_id': gererCommandesMenu.first['id'], |
|||
'permission_id': managePermission.first['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
|
|||
if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) { |
|||
// User peut voir les commandes |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': userRoleId, |
|||
'menu_id': gererCommandesMenu.first['id'], |
|||
'permission_id': viewPermission.first['id'], |
|||
} |
|||
, conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
} |
|||
Future<void> _updateExistingRolePermissions(Database db) async { |
|||
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']); |
|||
if (superAdminRole.isNotEmpty) { |
|||
final superAdminRoleId = superAdminRole.first['id'] as int; |
|||
final permissions = await db.query('permissions'); |
|||
final menus = await db.query('menu'); |
|||
|
|||
// Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus |
|||
for (var menu in menus) { |
|||
for (var permission in permissions) { |
|||
final existingPermission = await db.query( |
|||
'role_menu_permissions', |
|||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?', |
|||
whereArgs: [superAdminRoleId, menu['id'], permission['id']], |
|||
); |
|||
if (existingPermission.isEmpty) { |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': superAdminRoleId, |
|||
'menu_id': menu['id'], |
|||
'permission_id': permission['id'], |
|||
}, |
|||
conflictAlgorithm: ConflictAlgorithm.ignore |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Assigner les permissions de base aux autres rôles pour les nouveaux menus |
|||
final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']); |
|||
final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']); |
|||
|
|||
if (adminRole.isNotEmpty && userRole.isNotEmpty) { |
|||
await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int); |
|||
} |
|||
|
|||
print("Permissions mises à jour pour tous les rôles"); |
|||
} |
|||
} |
|||
|
|||
|
|||
Future<void> insertDefaultSuperAdmin() async { |
|||
final db = await database; |
|||
|
|||
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) { |
|||
final superAdminRole = await db.query('roles', |
|||
where: 'designation = ?', |
|||
whereArgs: ['Super Admin'] |
|||
); |
|||
|
|||
if (superAdminRole.isNotEmpty) { |
|||
final superAdminRoleId = superAdminRole.first['id'] as int; |
|||
|
|||
await db.insert('users', { |
|||
'name': 'Super', |
|||
'lastname': 'Admin', |
|||
'email': 'superadmin@youmazgestion.com', |
|||
'password': 'admin123', |
|||
'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à"); |
|||
} |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
|
|||
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], |
|||
); |
|||
} |
|||
|
|||
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], |
|||
); |
|||
} |
|||
|
|||
Future<void> assignMenuPermission(int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.insert('menu_permissions', { |
|||
'menu_id': menuId, |
|||
'permission_id': permissionId, |
|||
}, conflictAlgorithm: ConflictAlgorithm.ignore); |
|||
} |
|||
|
|||
Future<void> removeMenuPermission(int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.delete( |
|||
'menu_permissions', |
|||
where: 'menu_id = ? AND permission_id = ?', |
|||
whereArgs: [menuId, permissionId], |
|||
); |
|||
} |
|||
|
|||
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; |
|||
|
|||
final isValidOldPassword = await verifyUser(username, oldPassword); |
|||
if (!isValidOldPassword) { |
|||
throw Exception('Ancien mot de passe incorrect'); |
|||
} |
|||
|
|||
await db.update( |
|||
'users', |
|||
{'password': newPassword}, |
|||
where: 'username = ?', |
|||
whereArgs: [username], |
|||
); |
|||
} |
|||
|
|||
Future<bool> hasPermission(String username, String permissionName, String menuRoute) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT COUNT(*) as count |
|||
FROM permissions p |
|||
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id |
|||
JOIN roles r ON rmp.role_id = r.id |
|||
JOIN users u ON u.role_id = r.id |
|||
JOIN menu m ON m.route = ? |
|||
WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id |
|||
''', [menuRoute, username, permissionName]); |
|||
|
|||
return (result.first['count'] as int) > 0; |
|||
} |
|||
|
|||
|
|||
Future<void> close() async { |
|||
if (_database.isOpen) { |
|||
await _database.close(); |
|||
} |
|||
} |
|||
|
|||
Future<void> printDatabaseInfo() async { |
|||
final db = await database; |
|||
|
|||
print("=== INFORMATIONS DE LA BASE DE DONNÉES ==="); |
|||
|
|||
final userCount = await getUserCount(); |
|||
print("Nombre d'utilisateurs: $userCount"); |
|||
|
|||
final users = await getAllUsers(); |
|||
print("Utilisateurs:"); |
|||
for (var user in users) { |
|||
print(" - ${user.username} (${user.name} ) - Email: ${user.email}"); |
|||
} |
|||
|
|||
final roles = await getRoles(); |
|||
print("Rôles:"); |
|||
for (var role in roles) { |
|||
print(" - ${role.designation} (ID: ${role.id})"); |
|||
} |
|||
|
|||
final permissions = await getAllPermissions(); |
|||
print("Permissions:"); |
|||
for (var permission in permissions) { |
|||
print(" - ${permission.name} (ID: ${permission.id})"); |
|||
} |
|||
|
|||
print("========================================="); |
|||
} |
|||
|
|||
Future<List<Permission>> getPermissionsForRoleAndMenu(int roleId, int menuId) async { |
|||
final db = await database; |
|||
final result = await db.rawQuery(''' |
|||
SELECT p.id, p.name |
|||
FROM permissions p |
|||
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id |
|||
WHERE rmp.role_id = ? AND rmp.menu_id = ? |
|||
ORDER BY p.name ASC |
|||
''', [roleId, menuId]); |
|||
|
|||
return result.map((map) => Permission.fromMap(map)).toList(); |
|||
} |
|||
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue |
|||
Future<void> deleteDatabaseFile() async { |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, 'app_database.db'); |
|||
final file = File(path); |
|||
if (await file.exists()) { |
|||
await file.delete(); |
|||
print("Base de données utilisateur supprimée"); |
|||
} |
|||
} |
|||
Future<void> assignRoleMenuPermission(int roleId, int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.insert('role_menu_permissions', { |
|||
'role_id': roleId, |
|||
'menu_id': menuId, |
|||
'permission_id': permissionId, |
|||
}, conflictAlgorithm: ConflictAlgorithm.ignore); |
|||
} |
|||
|
|||
|
|||
|
|||
Future<void> removeRoleMenuPermission(int roleId, int menuId, int permissionId) async { |
|||
final db = await database; |
|||
await db.delete( |
|||
'role_menu_permissions', |
|||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?', |
|||
whereArgs: [roleId, menuId, permissionId], |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -1,559 +0,0 @@ |
|||
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/produit.dart'; |
|||
import '../Models/client.dart'; |
|||
|
|||
|
|||
class ProductDatabase { |
|||
static final ProductDatabase instance = ProductDatabase._init(); |
|||
late Database _database; |
|||
|
|||
ProductDatabase._init() { |
|||
sqfliteFfiInit(); |
|||
} |
|||
|
|||
ProductDatabase(); |
|||
|
|||
Future<Database> get database async { |
|||
if (_database.isOpen) return _database; |
|||
_database = await _initDB('products2.db'); |
|||
return _database; |
|||
} |
|||
|
|||
Future<void> initDatabase() async { |
|||
_database = await _initDB('products2.db'); |
|||
await _createDB(_database, 1); |
|||
await _insertDefaultClients(); |
|||
await _insertDefaultCommandes(); |
|||
} |
|||
|
|||
Future<Database> _initDB(String filePath) async { |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, filePath); |
|||
|
|||
bool dbExists = await File(path).exists(); |
|||
if (!dbExists) { |
|||
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) { |
|||
print('Pas de fichier DB dans assets, création 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 products (existante avec améliorations) |
|||
if (!tableNames.contains('products')) { |
|||
await db.execute(''' |
|||
CREATE TABLE products( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL, |
|||
price REAL NOT NULL, |
|||
image TEXT, |
|||
category TEXT NOT NULL, |
|||
stock INTEGER NOT NULL DEFAULT 0, |
|||
description TEXT, |
|||
qrCode TEXT, |
|||
reference TEXT UNIQUE |
|||
) |
|||
'''); |
|||
print("Table 'products' créée."); |
|||
} else { |
|||
// Vérifier et ajouter les colonnes manquantes |
|||
await _updateProductsTable(db); |
|||
} |
|||
|
|||
// Table clients |
|||
if (!tableNames.contains('clients')) { |
|||
await db.execute(''' |
|||
CREATE TABLE clients( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
nom TEXT NOT NULL, |
|||
prenom TEXT NOT NULL, |
|||
email TEXT NOT NULL UNIQUE, |
|||
telephone TEXT NOT NULL, |
|||
adresse TEXT, |
|||
dateCreation TEXT NOT NULL, |
|||
actif INTEGER NOT NULL DEFAULT 1 |
|||
) |
|||
'''); |
|||
print("Table 'clients' créée."); |
|||
} |
|||
|
|||
// Table commandes |
|||
if (!tableNames.contains('commandes')) { |
|||
await db.execute(''' |
|||
CREATE TABLE commandes( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
clientId INTEGER NOT NULL, |
|||
dateCommande TEXT NOT NULL, |
|||
statut INTEGER NOT NULL DEFAULT 0, |
|||
montantTotal REAL NOT NULL, |
|||
notes TEXT, |
|||
dateLivraison TEXT, |
|||
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'commandes' créée."); |
|||
} |
|||
|
|||
// Table détails commandes |
|||
if (!tableNames.contains('details_commandes')) { |
|||
await db.execute(''' |
|||
CREATE TABLE details_commandes( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
commandeId INTEGER NOT NULL, |
|||
produitId INTEGER NOT NULL, |
|||
quantite INTEGER NOT NULL, |
|||
prixUnitaire REAL NOT NULL, |
|||
sousTotal REAL NOT NULL, |
|||
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, |
|||
FOREIGN KEY (produitId) REFERENCES products(id) ON DELETE CASCADE |
|||
) |
|||
'''); |
|||
print("Table 'details_commandes' créée."); |
|||
} |
|||
|
|||
// Créer les index pour optimiser les performances |
|||
await _createIndexes(db); |
|||
} |
|||
|
|||
Future<void> _updateProductsTable(Database db) async { |
|||
final columns = await db.rawQuery('PRAGMA table_info(products)'); |
|||
final columnNames = columns.map((e) => e['name'] as String).toList(); |
|||
|
|||
if (!columnNames.contains('description')) { |
|||
await db.execute("ALTER TABLE products ADD COLUMN description TEXT"); |
|||
print("Colonne 'description' ajoutée."); |
|||
} |
|||
if (!columnNames.contains('qrCode')) { |
|||
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT"); |
|||
print("Colonne 'qrCode' ajoutée."); |
|||
} |
|||
if (!columnNames.contains('reference')) { |
|||
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT"); |
|||
print("Colonne 'reference' ajoutée."); |
|||
} |
|||
} |
|||
|
|||
Future<void> _createIndexes(Database db) async { |
|||
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)'); |
|||
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_reference ON products(reference)'); |
|||
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_client ON commandes(clientId)'); |
|||
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_date ON commandes(dateCommande)'); |
|||
await db.execute('CREATE INDEX IF NOT EXISTS idx_details_commande ON details_commandes(commandeId)'); |
|||
print("Index créés pour optimiser les performances."); |
|||
} |
|||
|
|||
// ========================= |
|||
// MÉTHODES PRODUCTS (existantes) |
|||
// ========================= |
|||
Future<int> createProduct(Product product) async { |
|||
final db = await database; |
|||
return await db.insert('products', product.toMap()); |
|||
} |
|||
|
|||
Future<List<Product>> getProducts() async { |
|||
final db = await database; |
|||
final maps = await db.query('products', orderBy: 'name ASC'); |
|||
return List.generate(maps.length, (i) { |
|||
return Product.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
Future<int> updateProduct(Product product) async { |
|||
final db = await database; |
|||
return await db.update( |
|||
'products', |
|||
product.toMap(), |
|||
where: 'id = ?', |
|||
whereArgs: [product.id], |
|||
); |
|||
} |
|||
|
|||
Future<int> deleteProduct(int? id) async { |
|||
final db = await database; |
|||
return await db.delete( |
|||
'products', |
|||
where: 'id = ?', |
|||
whereArgs: [id], |
|||
); |
|||
} |
|||
|
|||
Future<List<String>> getCategories() async { |
|||
final db = await database; |
|||
final result = await db.rawQuery('SELECT DISTINCT category FROM products ORDER BY category'); |
|||
return List.generate( |
|||
result.length, (index) => result[index]['category'] as String); |
|||
} |
|||
|
|||
Future<List<Product>> getProductsByCategory(String category) async { |
|||
final db = await database; |
|||
final maps = await db |
|||
.query('products', where: 'category = ?', whereArgs: [category], orderBy: 'name ASC'); |
|||
return List.generate(maps.length, (i) { |
|||
return Product.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
Future<int> updateStock(int id, int stock) async { |
|||
final db = await database; |
|||
return await db |
|||
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]); |
|||
} |
|||
|
|||
Future<Product?> getProductByReference(String reference) async { |
|||
final db = await database; |
|||
final maps = await db.query( |
|||
'products', |
|||
where: 'reference = ?', |
|||
whereArgs: [reference], |
|||
); |
|||
|
|||
if (maps.isNotEmpty) { |
|||
return Product.fromMap(maps.first); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
// ========================= |
|||
// MÉTHODES CLIENTS |
|||
// ========================= |
|||
Future<int> createClient(Client client) async { |
|||
final db = await database; |
|||
return await db.insert('clients', client.toMap()); |
|||
} |
|||
|
|||
Future<List<Client>> getClients() async { |
|||
final db = await database; |
|||
final maps = await db.query('clients', where: 'actif = 1', orderBy: 'nom ASC, prenom ASC'); |
|||
return List.generate(maps.length, (i) { |
|||
return Client.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
Future<Client?> getClientById(int id) async { |
|||
final db = await database; |
|||
final maps = await db.query('clients', where: 'id = ?', whereArgs: [id]); |
|||
if (maps.isNotEmpty) { |
|||
return Client.fromMap(maps.first); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
Future<int> updateClient(Client client) async { |
|||
final db = await database; |
|||
return await db.update( |
|||
'clients', |
|||
client.toMap(), |
|||
where: 'id = ?', |
|||
whereArgs: [client.id], |
|||
); |
|||
} |
|||
|
|||
Future<int> deleteClient(int id) async { |
|||
final db = await database; |
|||
// Soft delete |
|||
return await db.update( |
|||
'clients', |
|||
{'actif': 0}, |
|||
where: 'id = ?', |
|||
whereArgs: [id], |
|||
); |
|||
} |
|||
|
|||
Future<List<Client>> searchClients(String query) async { |
|||
final db = await database; |
|||
final maps = await db.query( |
|||
'clients', |
|||
where: 'actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?)', |
|||
whereArgs: ['%$query%', '%$query%', '%$query%'], |
|||
orderBy: 'nom ASC, prenom ASC', |
|||
); |
|||
return List.generate(maps.length, (i) { |
|||
return Client.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
// ========================= |
|||
// MÉTHODES COMMANDES |
|||
// ========================= |
|||
Future<int> createCommande(Commande commande) async { |
|||
final db = await database; |
|||
return await db.insert('commandes', commande.toMap()); |
|||
} |
|||
|
|||
Future<List<Commande>> getCommandes() async { |
|||
final db = await database; |
|||
final maps = await db.rawQuery(''' |
|||
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail |
|||
FROM commandes c |
|||
LEFT JOIN clients cl ON c.clientId = cl.id |
|||
ORDER BY c.dateCommande DESC |
|||
'''); |
|||
return List.generate(maps.length, (i) { |
|||
return Commande.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
Future<List<Commande>> getCommandesByClient(int clientId) async { |
|||
final db = await database; |
|||
final maps = await db.rawQuery(''' |
|||
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail |
|||
FROM commandes c |
|||
LEFT JOIN clients cl ON c.clientId = cl.id |
|||
WHERE c.clientId = ? |
|||
ORDER BY c.dateCommande DESC |
|||
''', [clientId]); |
|||
return List.generate(maps.length, (i) { |
|||
return Commande.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
Future<Commande?> getCommandeById(int id) async { |
|||
final db = await database; |
|||
final maps = await db.rawQuery(''' |
|||
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail |
|||
FROM commandes c |
|||
LEFT JOIN clients cl ON c.clientId = cl.id |
|||
WHERE c.id = ? |
|||
''', [id]); |
|||
if (maps.isNotEmpty) { |
|||
return Commande.fromMap(maps.first); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
Future<int> updateCommande(Commande commande) async { |
|||
final db = await database; |
|||
return await db.update( |
|||
'commandes', |
|||
commande.toMap(), |
|||
where: 'id = ?', |
|||
whereArgs: [commande.id], |
|||
); |
|||
} |
|||
|
|||
Future<int> updateStatutCommande(int commandeId, StatutCommande statut) async { |
|||
final db = await database; |
|||
return await db.update( |
|||
'commandes', |
|||
{'statut': statut.index}, |
|||
where: 'id = ?', |
|||
whereArgs: [commandeId], |
|||
); |
|||
} |
|||
|
|||
// ========================= |
|||
// MÉTHODES DÉTAILS COMMANDES |
|||
// ========================= |
|||
Future<int> createDetailCommande(DetailCommande detail) async { |
|||
final db = await database; |
|||
return await db.insert('details_commandes', detail.toMap()); |
|||
} |
|||
|
|||
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async { |
|||
final db = await database; |
|||
final maps = await db.rawQuery(''' |
|||
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference |
|||
FROM details_commandes dc |
|||
LEFT JOIN products p ON dc.produitId = p.id |
|||
WHERE dc.commandeId = ? |
|||
ORDER BY dc.id |
|||
''', [commandeId]); |
|||
return List.generate(maps.length, (i) { |
|||
return DetailCommande.fromMap(maps[i]); |
|||
}); |
|||
} |
|||
|
|||
// ========================= |
|||
// MÉTHODES TRANSACTION COMPLÈTE |
|||
// ========================= |
|||
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async { |
|||
final db = await database; |
|||
|
|||
return await db.transaction((txn) async { |
|||
// Créer le client |
|||
final clientId = await txn.insert('clients', client.toMap()); |
|||
|
|||
// Créer la commande |
|||
final commandeMap = commande.toMap(); |
|||
commandeMap['clientId'] = clientId; |
|||
final commandeId = await txn.insert('commandes', commandeMap); |
|||
|
|||
// Créer les détails et mettre à jour le stock |
|||
for (var detail in details) { |
|||
final detailMap = detail.toMap(); |
|||
detailMap['commandeId'] = commandeId; // Ajoute l'ID de la commande |
|||
await txn.insert('details_commandes', detailMap); |
|||
|
|||
// Mettre à jour le stock du produit |
|||
await txn.rawUpdate( |
|||
'UPDATE products SET stock = stock - ? WHERE id = ?', |
|||
[detail.quantite, detail.produitId], |
|||
); |
|||
} |
|||
|
|||
return commandeId; |
|||
}); |
|||
} |
|||
|
|||
// ========================= |
|||
// STATISTIQUES |
|||
// ========================= |
|||
Future<Map<String, dynamic>> getStatistiques() async { |
|||
final db = await database; |
|||
|
|||
final totalClients = await db.rawQuery('SELECT COUNT(*) as count FROM clients WHERE actif = 1'); |
|||
final totalCommandes = await db.rawQuery('SELECT COUNT(*) as count FROM commandes'); |
|||
final totalProduits = await db.rawQuery('SELECT COUNT(*) as count FROM products'); |
|||
final chiffreAffaires = await db.rawQuery('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); // 5 = annulée |
|||
|
|||
return { |
|||
'totalClients': totalClients.first['count'], |
|||
'totalCommandes': totalCommandes.first['count'], |
|||
'totalProduits': totalProduits.first['count'], |
|||
'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0, |
|||
}; |
|||
} |
|||
|
|||
// ========================= |
|||
// DONNÉES PAR DÉFAUT |
|||
// ========================= |
|||
Future<void> _insertDefaultClients() async { |
|||
final db = await database; |
|||
final existingClients = await db.query('clients'); |
|||
|
|||
if (existingClients.isEmpty) { |
|||
final defaultClients = [ |
|||
Client( |
|||
nom: 'Dupont', |
|||
prenom: 'Jean', |
|||
email: 'jean.dupont@email.com', |
|||
telephone: '0123456789', |
|||
adresse: '123 Rue de la Paix, Paris', |
|||
dateCreation: DateTime.now(), |
|||
), |
|||
Client( |
|||
nom: 'Martin', |
|||
prenom: 'Marie', |
|||
email: 'marie.martin@email.com', |
|||
telephone: '0987654321', |
|||
adresse: '456 Avenue des Champs, Lyon', |
|||
dateCreation: DateTime.now(), |
|||
), |
|||
Client( |
|||
nom: 'Bernard', |
|||
prenom: 'Pierre', |
|||
email: 'pierre.bernard@email.com', |
|||
telephone: '0456789123', |
|||
adresse: '789 Boulevard Saint-Michel, Marseille', |
|||
dateCreation: DateTime.now(), |
|||
), |
|||
]; |
|||
|
|||
for (var client in defaultClients) { |
|||
await db.insert('clients', client.toMap()); |
|||
} |
|||
print("Clients par défaut insérés"); |
|||
} |
|||
} |
|||
|
|||
Future<void> _insertDefaultCommandes() async { |
|||
final db = await database; |
|||
final existingCommandes = await db.query('commandes'); |
|||
|
|||
if (existingCommandes.isEmpty) { |
|||
// Récupérer quelques produits pour créer des commandes |
|||
final produits = await db.query('products', limit: 3); |
|||
final clients = await db.query('clients', limit: 3); |
|||
|
|||
if (produits.isNotEmpty && clients.isNotEmpty) { |
|||
// Commande 1 |
|||
final commande1Id = await db.insert('commandes', { |
|||
'clientId': clients[0]['id'], |
|||
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(), |
|||
'statut': StatutCommande.livree.index, |
|||
'montantTotal': 150.0, |
|||
'notes': 'Commande urgente', |
|||
}); |
|||
|
|||
await db.insert('details_commandes', { |
|||
'commandeId': commande1Id, |
|||
'produitId': produits[0]['id'], |
|||
'quantite': 2, |
|||
'prixUnitaire': 75.0, |
|||
'sousTotal': 150.0, |
|||
}); |
|||
|
|||
// Commande 2 |
|||
final commande2Id = await db.insert('commandes', { |
|||
'clientId': clients[1]['id'], |
|||
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(), |
|||
'statut': StatutCommande.enPreparation.index, |
|||
'montantTotal': 225.0, |
|||
'notes': 'Livraison prévue demain', |
|||
}); |
|||
|
|||
if (produits.length > 1) { |
|||
await db.insert('details_commandes', { |
|||
'commandeId': commande2Id, |
|||
'produitId': produits[1]['id'], |
|||
'quantite': 3, |
|||
'prixUnitaire': 75.0, |
|||
'sousTotal': 225.0, |
|||
}); |
|||
} |
|||
|
|||
// Commande 3 |
|||
final commande3Id = await db.insert('commandes', { |
|||
'clientId': clients[2]['id'], |
|||
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(), |
|||
'statut': StatutCommande.confirmee.index, |
|||
'montantTotal': 300.0, |
|||
'notes': 'Commande standard', |
|||
}); |
|||
|
|||
if (produits.length > 2) { |
|||
await db.insert('details_commandes', { |
|||
'commandeId': commande3Id, |
|||
'produitId': produits[2]['id'], |
|||
'quantite': 4, |
|||
'prixUnitaire': 75.0, |
|||
'sousTotal': 300.0, |
|||
}); |
|||
} |
|||
|
|||
print("Commandes par défaut insérées"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> close() async { |
|||
if (_database.isOpen) { |
|||
await _database.close(); |
|||
} |
|||
} |
|||
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue |
|||
Future<void> deleteDatabaseFile() async { |
|||
final documentsDirectory = await getApplicationDocumentsDirectory(); |
|||
final path = join(documentsDirectory.path, 'products2.db'); |
|||
final file = File(path); |
|||
if (await file.exists()) { |
|||
await file.delete(); |
|||
print("Base de données product supprimée"); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,123 +1,403 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:intl/intl.dart'; |
|||
import '../Components/appDrawer.dart'; |
|||
import '../controller/HistoryController.dart'; |
|||
import 'listCommandeHistory.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
|
|||
class HistoriquePage extends StatefulWidget { |
|||
const HistoriquePage({super.key}); |
|||
|
|||
class HistoryPage extends GetView<HistoryController> { |
|||
@override |
|||
HistoryController controller = Get.put(HistoryController()); |
|||
_HistoriquePageState createState() => _HistoriquePageState(); |
|||
} |
|||
|
|||
HistoryPage({super.key}); |
|||
class _HistoriquePageState extends State<HistoriquePage> { |
|||
final AppDatabase _appDatabase = AppDatabase.instance; |
|||
List<Commande> _commandes = []; |
|||
bool _isLoading = true; |
|||
DateTimeRange? _dateRange; |
|||
final TextEditingController _searchController = TextEditingController(); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: const CustomAppBar(title: 'Historique'), |
|||
drawer: CustomDrawer(), |
|||
body: Column( |
|||
children: [ |
|||
Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: ElevatedButton( |
|||
onPressed: () { |
|||
controller.refreshOrders(); |
|||
controller.onInit(); |
|||
}, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.deepOrangeAccent, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(20.0), |
|||
void initState() { |
|||
super.initState(); |
|||
_loadCommandes(); |
|||
} |
|||
|
|||
Future<void> _loadCommandes() async { |
|||
setState(() { |
|||
_isLoading = true; |
|||
}); |
|||
|
|||
try { |
|||
final commandes = await _appDatabase.getCommandes(); |
|||
setState(() { |
|||
_commandes = commandes; |
|||
_isLoading = false; |
|||
}); |
|||
} catch (e) { |
|||
setState(() { |
|||
_isLoading = false; |
|||
}); |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Impossible de charger les commandes: ${e.toString()}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
} |
|||
|
|||
Future<void> _selectDateRange(BuildContext context) async { |
|||
final DateTimeRange? picked = await showDateRangePicker( |
|||
context: context, |
|||
firstDate: DateTime(2020), |
|||
lastDate: DateTime.now().add(const Duration(days: 365)), |
|||
initialDateRange: _dateRange ?? DateTimeRange( |
|||
start: DateTime.now().subtract(const Duration(days: 30)), |
|||
end: DateTime.now(), |
|||
), |
|||
); |
|||
|
|||
if (picked != null) { |
|||
setState(() { |
|||
_dateRange = picked; |
|||
}); |
|||
_filterCommandes(); |
|||
} |
|||
} |
|||
|
|||
void _filterCommandes() { |
|||
final searchText = _searchController.text.toLowerCase(); |
|||
setState(() { |
|||
_isLoading = true; |
|||
}); |
|||
|
|||
_appDatabase.getCommandes().then((commandes) { |
|||
List<Commande> filtered = commandes; |
|||
|
|||
// Filtre par date |
|||
if (_dateRange != null) { |
|||
filtered = filtered.where((commande) { |
|||
final date = commande.dateCommande; |
|||
return date.isAfter(_dateRange!.start) && |
|||
date.isBefore(_dateRange!.end.add(const Duration(days: 1))); |
|||
}).toList(); |
|||
} |
|||
|
|||
// Filtre par recherche |
|||
if (searchText.isNotEmpty) { |
|||
filtered = filtered.where((commande) { |
|||
return commande.clientNom!.toLowerCase().contains(searchText) || |
|||
commande.clientPrenom!.toLowerCase().contains(searchText) || |
|||
commande.id.toString().contains(searchText); |
|||
}).toList(); |
|||
} |
|||
|
|||
setState(() { |
|||
_commandes = filtered; |
|||
_isLoading = false; |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
void _showCommandeDetails(Commande commande) async { |
|||
final details = await _appDatabase.getDetailsCommande(commande.id!); |
|||
final client = await _appDatabase.getClientById(commande.clientId); |
|||
|
|||
Get.bottomSheet( |
|||
Container( |
|||
padding: const EdgeInsets.all(16), |
|||
height: MediaQuery.of(context).size.height * 0.7, |
|||
decoration: const BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
Text( |
|||
'Commande #${commande.id}', |
|||
style: const TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
), |
|||
child: const Text( |
|||
'Rafraîchir', |
|||
style: TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 16.0, |
|||
IconButton( |
|||
icon: const Icon(Icons.close), |
|||
onPressed: () => Get.back(), |
|||
), |
|||
], |
|||
), |
|||
const Divider(), |
|||
Text( |
|||
'Client: ${client?.nom} ${client?.prenom}', |
|||
style: const TextStyle(fontSize: 16), |
|||
), |
|||
Text( |
|||
'Date: ${commande.dateCommande}', |
|||
style: const TextStyle(fontSize: 16), |
|||
), |
|||
Text( |
|||
'Statut: ${_getStatutText(commande.statut)}', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
color: _getStatutColor(commande.statut), |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
), |
|||
Expanded( |
|||
child: Obx( |
|||
() { |
|||
final distinctDates = controller.workDays; |
|||
|
|||
if (distinctDates.isEmpty) { |
|||
return const Center( |
|||
child: Text( |
|||
'Aucune journée de travail trouvée', |
|||
style: TextStyle( |
|||
fontSize: 18.0, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
const SizedBox(height: 16), |
|||
const Text( |
|||
'Articles:', |
|||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), |
|||
), |
|||
Expanded( |
|||
child: ListView.builder( |
|||
itemCount: details.length, |
|||
itemBuilder: (context, index) { |
|||
final detail = details[index]; |
|||
return ListTile( |
|||
leading: const Icon(Icons.shopping_bag), |
|||
title: Text(detail.produitNom ?? 'Produit inconnu'), |
|||
subtitle: Text( |
|||
'${detail.quantite} x ${detail.prixUnitaire.toStringAsFixed(2)} DA', |
|||
), |
|||
trailing: Text( |
|||
'${detail.sousTotal.toStringAsFixed(2)} DA', |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
); |
|||
} |
|||
|
|||
return ListView.builder( |
|||
itemCount: distinctDates.length, |
|||
itemBuilder: (context, index) { |
|||
final date = distinctDates[index]; |
|||
return Card( |
|||
elevation: 2.0, |
|||
margin: const EdgeInsets.symmetric( |
|||
horizontal: 16.0, |
|||
vertical: 8.0, |
|||
), |
|||
child: ListTile( |
|||
title: Text( |
|||
'Journée du $date', |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
leading: const CircleAvatar( |
|||
backgroundColor: Colors.deepOrange, |
|||
child: Icon( |
|||
Icons.calendar_today, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
trailing: const Icon( |
|||
Icons.arrow_forward, |
|||
color: Colors.deepOrange, |
|||
), |
|||
onTap: () => navigateToDetailPage(date), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
}, |
|||
}, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
const Divider(), |
|||
Padding( |
|||
padding: const EdgeInsets.symmetric(vertical: 8), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Total:', |
|||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), |
|||
), |
|||
Text( |
|||
'${commande.montantTotal.toStringAsFixed(2)} DA', |
|||
style: const TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.green, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
if (commande.statut == StatutCommande.enAttente || |
|||
commande.statut == StatutCommande.enPreparation) |
|||
ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue.shade800, |
|||
foregroundColor: Colors.white, |
|||
padding: const EdgeInsets.symmetric(vertical: 16), |
|||
), |
|||
onPressed: () => _updateStatutCommande(commande.id!), |
|||
child: const Text('Marquer comme livrée'), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
String formatDate(String date) { |
|||
String _getStatutText(StatutCommande statut) { |
|||
switch (statut) { |
|||
case StatutCommande.enAttente: |
|||
return 'En attente'; |
|||
case StatutCommande.confirmee: |
|||
return 'Confirmée'; |
|||
case StatutCommande.enPreparation: |
|||
return 'En préparation'; |
|||
case StatutCommande.livree: |
|||
return 'Livrée'; |
|||
case StatutCommande.annulee: |
|||
return 'Annulée'; |
|||
default: |
|||
return 'Inconnu'; |
|||
} |
|||
} |
|||
|
|||
Color _getStatutColor(StatutCommande statut) { |
|||
switch (statut) { |
|||
case StatutCommande.enAttente: |
|||
return Colors.orange; |
|||
case StatutCommande.confirmee: |
|||
return Colors.blue; |
|||
case StatutCommande.enPreparation: |
|||
return Colors.purple; |
|||
case StatutCommande.livree: |
|||
return Colors.green; |
|||
case StatutCommande.annulee: |
|||
return Colors.red; |
|||
default: |
|||
return Colors.grey; |
|||
} |
|||
} |
|||
|
|||
Future<void> _updateStatutCommande(int commandeId) async { |
|||
try { |
|||
final parsedDate = DateFormat('dd-MM-yyyy').parse(date); |
|||
print('parsedDate1: $parsedDate'); |
|||
final formattedDate = DateFormat('yyyy-MM-dd').format(parsedDate); |
|||
print('formattedDate1: $formattedDate'); |
|||
return formattedDate; |
|||
await _appDatabase.updateStatutCommande( |
|||
commandeId, StatutCommande.livree); |
|||
Get.back(); // Ferme le bottom sheet |
|||
_loadCommandes(); |
|||
Get.snackbar( |
|||
'Succès', |
|||
'Statut de la commande mis à jour', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.green, |
|||
colorText: Colors.white, |
|||
); |
|||
} catch (e) { |
|||
print('Error parsing date: $date'); |
|||
return ''; |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Impossible de mettre à jour le statut: ${e.toString()}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
} |
|||
|
|||
// transformer string en DateTime |
|||
void navigateToDetailPage(String selectedDate) { |
|||
print('selectedDate: $selectedDate'); |
|||
DateTime parsedDate = DateFormat('yyyy-MM-dd').parse(selectedDate); |
|||
print('parsedDate: $parsedDate'); |
|||
|
|||
Get.to(() => HistoryDetailPage(selectedDate: parsedDate)); |
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: AppBar( |
|||
title: const Text('Historique des Commandes'), |
|||
actions: [ |
|||
IconButton( |
|||
icon: const Icon(Icons.refresh), |
|||
onPressed: _loadCommandes, |
|||
), |
|||
], |
|||
), |
|||
body: Column( |
|||
children: [ |
|||
Padding( |
|||
padding: const EdgeInsets.all(8), |
|||
child: Row( |
|||
children: [ |
|||
Expanded( |
|||
child: TextField( |
|||
controller: _searchController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Rechercher', |
|||
prefixIcon: const Icon(Icons.search), |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
suffixIcon: IconButton( |
|||
icon: const Icon(Icons.clear), |
|||
onPressed: () { |
|||
_searchController.clear(); |
|||
_filterCommandes(); |
|||
}, |
|||
), |
|||
), |
|||
onChanged: (value) => _filterCommandes(), |
|||
), |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.date_range), |
|||
onPressed: () => _selectDateRange(context), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
if (_dateRange != null) |
|||
Padding( |
|||
padding: const EdgeInsets.symmetric(horizontal: 16), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Text( |
|||
DateFormat('dd/MM/yyyy').format(_dateRange!.start), |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
const Text(' - '), |
|||
Text( |
|||
DateFormat('dd/MM/yyyy').format(_dateRange!.end), |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.close, size: 18), |
|||
onPressed: () { |
|||
setState(() { |
|||
_dateRange = null; |
|||
}); |
|||
_filterCommandes(); |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
Expanded( |
|||
child: _isLoading |
|||
? const Center(child: CircularProgressIndicator()) |
|||
: _commandes.isEmpty |
|||
? const Center( |
|||
child: Text('Aucune commande trouvée'), |
|||
) |
|||
: ListView.builder( |
|||
itemCount: _commandes.length, |
|||
itemBuilder: (context, index) { |
|||
final commande = _commandes[index]; |
|||
return Card( |
|||
margin: const EdgeInsets.symmetric( |
|||
horizontal: 8, vertical: 4), |
|||
child: ListTile( |
|||
leading: const Icon(Icons.shopping_cart), |
|||
title: Text( |
|||
'Commande #${commande.id} - ${commande.clientNom} ${commande.clientPrenom}'), |
|||
subtitle: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
commande.dateCommande.timeZoneName |
|||
), |
|||
Text( |
|||
'${commande.montantTotal.toStringAsFixed(2)} DA', |
|||
style: const TextStyle( |
|||
fontWeight: FontWeight.bold), |
|||
), |
|||
], |
|||
), |
|||
trailing: Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 8, vertical: 4), |
|||
decoration: BoxDecoration( |
|||
color: _getStatutColor(commande.statut) |
|||
.withOpacity(0.2), |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Text( |
|||
_getStatutText(commande.statut), |
|||
style: TextStyle( |
|||
color: _getStatutColor(commande.statut), |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
), |
|||
onTap: () => _showCommandeDetails(commande), |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,837 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Components/QrScan.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:youmazgestion/Components/appDrawer.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Models/users.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
import 'package:youmazgestion/Views/historique.dart'; |
|||
|
|||
void main() { |
|||
runApp(const MyApp()); |
|||
} |
|||
|
|||
class MyApp extends StatelessWidget { |
|||
const MyApp({super.key}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return GetMaterialApp( |
|||
title: 'Youmaz Gestion', |
|||
theme: ThemeData( |
|||
primarySwatch: Colors.blue, |
|||
visualDensity: VisualDensity.adaptivePlatformDensity, |
|||
), |
|||
home: const MainLayout(), |
|||
debugShowCheckedModeBanner: false, |
|||
); |
|||
} |
|||
} |
|||
|
|||
class MainLayout extends StatefulWidget { |
|||
const MainLayout({super.key}); |
|||
|
|||
@override |
|||
State<MainLayout> createState() => _MainLayoutState(); |
|||
} |
|||
|
|||
class _MainLayoutState extends State<MainLayout> { |
|||
int _currentIndex = 1; // Index par défaut pour la page de commande |
|||
|
|||
final List<Widget> _pages = [ |
|||
const HistoriquePage(), |
|||
const NouvelleCommandePage(), // Page 1 - Nouvelle commande |
|||
const ScanQRPage(), // Page 2 - Scan QR |
|||
]; |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: _currentIndex == 1 ? CustomAppBar(title: 'Nouvelle Commande') : null, |
|||
drawer: CustomDrawer(), |
|||
body: _pages[_currentIndex], |
|||
bottomNavigationBar: _buildAdaptiveBottomNavBar(), |
|||
); |
|||
} |
|||
|
|||
Widget _buildAdaptiveBottomNavBar() { |
|||
final isDesktop = MediaQuery.of(context).size.width > 600; |
|||
|
|||
return Container( |
|||
decoration: BoxDecoration( |
|||
border: isDesktop |
|||
? const Border(top: BorderSide(color: Colors.grey, width: 0.5)) |
|||
: null, |
|||
), |
|||
child: BottomNavigationBar( |
|||
currentIndex: _currentIndex, |
|||
onTap: (index) { |
|||
setState(() { |
|||
_currentIndex = index; |
|||
}); |
|||
}, |
|||
// Style adapté pour desktop |
|||
type: isDesktop ? BottomNavigationBarType.fixed : BottomNavigationBarType.fixed, |
|||
selectedFontSize: isDesktop ? 14 : 12, |
|||
unselectedFontSize: isDesktop ? 14 : 12, |
|||
iconSize: isDesktop ? 28 : 24, |
|||
items: const [ |
|||
BottomNavigationBarItem( |
|||
icon: Icon(Icons.history), |
|||
label: 'Historique', |
|||
), |
|||
BottomNavigationBarItem( |
|||
icon: Icon(Icons.add_shopping_cart), |
|||
label: 'Commande', |
|||
), |
|||
BottomNavigationBarItem( |
|||
icon: Icon(Icons.qr_code_scanner), |
|||
label: 'Scan QR', |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Votre code existant pour NouvelleCommandePage reste inchangé |
|||
class NouvelleCommandePage extends StatefulWidget { |
|||
const NouvelleCommandePage({super.key}); |
|||
|
|||
@override |
|||
_NouvelleCommandePageState createState() => _NouvelleCommandePageState(); |
|||
} |
|||
|
|||
class _NouvelleCommandePageState extends State<NouvelleCommandePage> { |
|||
final AppDatabase _appDatabase = AppDatabase.instance; |
|||
final _formKey = GlobalKey<FormState>(); |
|||
bool _isLoading = false; |
|||
|
|||
// Contrôleurs client |
|||
final TextEditingController _nomController = TextEditingController(); |
|||
final TextEditingController _prenomController = TextEditingController(); |
|||
final TextEditingController _emailController = TextEditingController(); |
|||
final TextEditingController _telephoneController = TextEditingController(); |
|||
final TextEditingController _adresseController = TextEditingController(); |
|||
|
|||
// Panier |
|||
final List<Product> _products = []; |
|||
final Map<int, int> _quantites = {}; |
|||
|
|||
// Utilisateurs commerciaux |
|||
List<Users> _commercialUsers = []; |
|||
Users? _selectedCommercialUser; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadProducts(); |
|||
_loadCommercialUsers(); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
final products = await _appDatabase.getProducts(); |
|||
setState(() { |
|||
_products.addAll(products); |
|||
}); |
|||
} |
|||
|
|||
Future<void> _loadCommercialUsers() async { |
|||
final commercialUsers = await _appDatabase.getCommercialUsers(); |
|||
setState(() { |
|||
_commercialUsers = commercialUsers; |
|||
if (_commercialUsers.isNotEmpty) { |
|||
_selectedCommercialUser = _commercialUsers.first; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
floatingActionButton: _buildFloatingCartButton(), |
|||
drawer: MediaQuery.of(context).size.width > 600 ? null : CustomDrawer(), |
|||
body: Column( |
|||
children: [ |
|||
// Header |
|||
Container( |
|||
padding: const EdgeInsets.all(16.0), |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
colors: [Colors.blue.shade800, Colors.blue.shade600], |
|||
begin: Alignment.topCenter, |
|||
end: Alignment.bottomCenter, |
|||
), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.1), |
|||
blurRadius: 6, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Container( |
|||
width: 50, |
|||
height: 50, |
|||
decoration: BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Icon( |
|||
Icons.shopping_cart, |
|||
color: Colors.blue, |
|||
size: 30, |
|||
), |
|||
), |
|||
const SizedBox(width: 12), |
|||
const Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Nouvelle Commande', |
|||
style: TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
Text( |
|||
'Créez une nouvelle commande pour un client', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.white70, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
// Contenu principal |
|||
Expanded( |
|||
child: SingleChildScrollView( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
padding: const EdgeInsets.symmetric(vertical: 16), |
|||
backgroundColor: Colors.blue.shade800, |
|||
foregroundColor: Colors.white, |
|||
), |
|||
onPressed: _showClientFormDialog, |
|||
child: const Text('Ajouter les informations client'), |
|||
), |
|||
const SizedBox(height: 20), |
|||
_buildProductList(), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildFloatingCartButton() { |
|||
return FloatingActionButton.extended( |
|||
onPressed: () { |
|||
_showCartBottomSheet(); |
|||
}, |
|||
icon: const Icon(Icons.shopping_cart), |
|||
label: Text('Panier (${_quantites.values.where((q) => q > 0).length})'), |
|||
backgroundColor: Colors.blue.shade800, |
|||
foregroundColor: Colors.white, |
|||
); |
|||
} |
|||
|
|||
void _showClientFormDialog() { |
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: const Text('Informations Client'), |
|||
content: SingleChildScrollView( |
|||
child: Form( |
|||
key: _formKey, |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
_buildTextFormField( |
|||
controller: _nomController, |
|||
label: 'Nom', |
|||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, |
|||
), |
|||
const SizedBox(height: 12), |
|||
_buildTextFormField( |
|||
controller: _prenomController, |
|||
label: 'Prénom', |
|||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, |
|||
), |
|||
const SizedBox(height: 12), |
|||
_buildTextFormField( |
|||
controller: _emailController, |
|||
label: 'Email', |
|||
keyboardType: TextInputType.emailAddress, |
|||
validator: (value) { |
|||
if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; |
|||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { |
|||
return 'Email invalide'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 12), |
|||
_buildTextFormField( |
|||
controller: _telephoneController, |
|||
label: 'Téléphone', |
|||
keyboardType: TextInputType.phone, |
|||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, |
|||
), |
|||
const SizedBox(height: 12), |
|||
_buildTextFormField( |
|||
controller: _adresseController, |
|||
label: 'Adresse', |
|||
maxLines: 2, |
|||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, |
|||
), |
|||
const SizedBox(height: 12), |
|||
_buildCommercialDropdown(), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Get.back(), |
|||
child: const Text('Annuler'), |
|||
), |
|||
ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.blue.shade800, |
|||
foregroundColor: Colors.white, |
|||
), |
|||
onPressed: () { |
|||
if (_formKey.currentState!.validate()) { |
|||
Get.back(); |
|||
Get.snackbar( |
|||
'Succès', |
|||
'Informations client enregistrées', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.green, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
}, |
|||
child: const Text('Enregistrer'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTextFormField({ |
|||
required TextEditingController controller, |
|||
required String label, |
|||
TextInputType? keyboardType, |
|||
String? Function(String?)? validator, |
|||
int? maxLines, |
|||
}) { |
|||
return TextFormField( |
|||
controller: controller, |
|||
decoration: InputDecoration( |
|||
labelText: label, |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
borderSide: BorderSide(color: Colors.grey.shade400), |
|||
), |
|||
enabledBorder: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
borderSide: BorderSide(color: Colors.grey.shade400), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|||
), |
|||
keyboardType: keyboardType, |
|||
validator: validator, |
|||
maxLines: maxLines, |
|||
); |
|||
} |
|||
|
|||
Widget _buildCommercialDropdown() { |
|||
return DropdownButtonFormField<Users>( |
|||
value: _selectedCommercialUser, |
|||
decoration: InputDecoration( |
|||
labelText: 'Commercial', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
), |
|||
items: _commercialUsers.map((Users user) { |
|||
return DropdownMenuItem<Users>( |
|||
value: user, |
|||
child: Text('${user.name} ${user.lastName}'), |
|||
); |
|||
}).toList(), |
|||
onChanged: (Users? newValue) { |
|||
setState(() { |
|||
_selectedCommercialUser = newValue; |
|||
}); |
|||
}, |
|||
validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null, |
|||
); |
|||
} |
|||
|
|||
Widget _buildProductList() { |
|||
return Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Produits Disponibles', |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Color.fromARGB(255, 9, 56, 95), |
|||
), |
|||
), |
|||
const SizedBox(height: 16), |
|||
_products.isEmpty |
|||
? const Center(child: CircularProgressIndicator()) |
|||
: ListView.builder( |
|||
shrinkWrap: true, |
|||
physics: const NeverScrollableScrollPhysics(), |
|||
itemCount: _products.length, |
|||
itemBuilder: (context, index) { |
|||
final product = _products[index]; |
|||
final quantity = _quantites[product.id] ?? 0; |
|||
|
|||
return _buildProductListItem(product, quantity); |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildProductListItem(Product product, int quantity) { |
|||
return Card( |
|||
margin: const EdgeInsets.symmetric(vertical: 8), |
|||
elevation: 2, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: ListTile( |
|||
contentPadding: const EdgeInsets.symmetric( |
|||
horizontal: 16, |
|||
vertical: 8, |
|||
), |
|||
leading: Container( |
|||
width: 50, |
|||
height: 50, |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Icon(Icons.shopping_bag, color: Colors.blue), |
|||
), |
|||
title: Text( |
|||
product.name, |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
subtitle: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const SizedBox(height: 4), |
|||
Text( |
|||
'${product.price.toStringAsFixed(2)} DA', |
|||
style: TextStyle( |
|||
color: Colors.green.shade700, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
if (product.stock != null) |
|||
Text( |
|||
'Stock: ${product.stock}', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.grey.shade600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
trailing: Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(20), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
IconButton( |
|||
icon: const Icon(Icons.remove, size: 18), |
|||
onPressed: () { |
|||
if (quantity > 0) { |
|||
setState(() { |
|||
_quantites[product.id!] = quantity - 1; |
|||
}); |
|||
} |
|||
}, |
|||
), |
|||
Text( |
|||
quantity.toString(), |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.add, size: 18), |
|||
onPressed: () { |
|||
if (product.stock == null || quantity < product.stock!) { |
|||
setState(() { |
|||
_quantites[product.id!] = quantity + 1; |
|||
}); |
|||
} else { |
|||
Get.snackbar( |
|||
'Stock insuffisant', |
|||
'Quantité demandée non disponible', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showCartBottomSheet() { |
|||
Get.bottomSheet( |
|||
Container( |
|||
height: MediaQuery.of(context).size.height * 0.7, |
|||
padding: const EdgeInsets.all(16), |
|||
decoration: const BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Votre Panier', |
|||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.close), |
|||
onPressed: () => Get.back(), |
|||
), |
|||
], |
|||
), |
|||
const Divider(), |
|||
Expanded(child: _buildCartItemsList()), |
|||
const Divider(), |
|||
_buildCartTotalSection(), |
|||
const SizedBox(height: 16), |
|||
_buildSubmitButton(), |
|||
], |
|||
), |
|||
), |
|||
isScrollControlled: true, |
|||
); |
|||
} |
|||
|
|||
Widget _buildCartItemsList() { |
|||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); |
|||
|
|||
if (itemsInCart.isEmpty) { |
|||
return const Center( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), |
|||
SizedBox(height: 16), |
|||
Text( |
|||
'Votre panier est vide', |
|||
style: TextStyle(fontSize: 16, color: Colors.grey), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
return ListView.builder( |
|||
itemCount: itemsInCart.length, |
|||
itemBuilder: (context, index) { |
|||
final entry = itemsInCart[index]; |
|||
final product = _products.firstWhere((p) => p.id == entry.key); |
|||
|
|||
return Dismissible( |
|||
key: Key(entry.key.toString()), |
|||
background: Container( |
|||
color: Colors.red.shade100, |
|||
alignment: Alignment.centerRight, |
|||
padding: const EdgeInsets.only(right: 20), |
|||
child: const Icon(Icons.delete, color: Colors.red), |
|||
), |
|||
direction: DismissDirection.endToStart, |
|||
onDismissed: (direction) { |
|||
setState(() { |
|||
_quantites.remove(entry.key); |
|||
}); |
|||
Get.snackbar( |
|||
'Produit retiré', |
|||
'${product.name} a été retiré du panier', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
); |
|||
}, |
|||
child: Card( |
|||
margin: const EdgeInsets.only(bottom: 8), |
|||
elevation: 1, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: ListTile( |
|||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |
|||
leading: Container( |
|||
width: 40, |
|||
height: 40, |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Icon(Icons.shopping_bag, size: 20), |
|||
), |
|||
title: Text(product.name), |
|||
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} DA'), |
|||
trailing: Text( |
|||
'${(entry.value * product.price).toStringAsFixed(2)} DA', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.blue.shade800, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
Widget _buildCartTotalSection() { |
|||
double total = 0; |
|||
_quantites.forEach((productId, quantity) { |
|||
final product = _products.firstWhere((p) => p.id == productId); |
|||
total += quantity * product.price; |
|||
}); |
|||
|
|||
return Column( |
|||
children: [ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Total:', |
|||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), |
|||
), |
|||
Text( |
|||
'${total.toStringAsFixed(2)} DA', |
|||
style: const TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.green, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'${_quantites.values.where((q) => q > 0).length} article(s)', |
|||
style: TextStyle(color: Colors.grey.shade600), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildSubmitButton() { |
|||
return SizedBox( |
|||
width: double.infinity, |
|||
child: ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
padding: const EdgeInsets.symmetric(vertical: 16), |
|||
backgroundColor: Colors.blue.shade800, |
|||
foregroundColor: Colors.white, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
elevation: 4, |
|||
), |
|||
onPressed: _submitOrder, |
|||
child: _isLoading |
|||
? const SizedBox( |
|||
width: 20, |
|||
height: 20, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.white, |
|||
), |
|||
) |
|||
: const Text( |
|||
'Valider la Commande', |
|||
style: TextStyle(fontSize: 16), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Future<void> _submitOrder() async { |
|||
if (_nomController.text.isEmpty || |
|||
_prenomController.text.isEmpty || |
|||
_emailController.text.isEmpty || |
|||
_telephoneController.text.isEmpty || |
|||
_adresseController.text.isEmpty) { |
|||
Get.back(); // Ferme le bottom sheet |
|||
Get.snackbar( |
|||
'Informations manquantes', |
|||
'Veuillez remplir les informations client', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
_showClientFormDialog(); |
|||
return; |
|||
} |
|||
|
|||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); |
|||
if (itemsInCart.isEmpty) { |
|||
Get.snackbar( |
|||
'Panier vide', |
|||
'Veuillez ajouter des produits à votre commande', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
return; |
|||
} |
|||
|
|||
setState(() { |
|||
_isLoading = true; |
|||
}); |
|||
|
|||
// Créer le client |
|||
final client = Client( |
|||
nom: _nomController.text, |
|||
prenom: _prenomController.text, |
|||
email: _emailController.text, |
|||
telephone: _telephoneController.text, |
|||
adresse: _adresseController.text, |
|||
dateCreation: DateTime.now(), |
|||
); |
|||
|
|||
// Calculer le total et préparer les détails |
|||
double total = 0; |
|||
final details = <DetailCommande>[]; |
|||
|
|||
for (final entry in itemsInCart) { |
|||
final product = _products.firstWhere((p) => p.id == entry.key); |
|||
total += entry.value * product.price; |
|||
|
|||
details.add(DetailCommande( |
|||
commandeId: 0, |
|||
produitId: product.id!, |
|||
quantite: entry.value, |
|||
prixUnitaire: product.price, |
|||
sousTotal: entry.value * product.price, |
|||
)); |
|||
} |
|||
|
|||
// Créer la commande |
|||
final commande = Commande( |
|||
clientId: 0, |
|||
dateCommande: DateTime.now(), |
|||
statut: StatutCommande.enAttente, |
|||
montantTotal: total, |
|||
notes: 'Commande passée via l\'application', |
|||
commandeurId: _selectedCommercialUser?.id, |
|||
); |
|||
|
|||
try { |
|||
await _appDatabase.createCommandeComplete(client, commande, details); |
|||
|
|||
Get.back(); // Ferme le bottom sheet |
|||
|
|||
// Afficher le dialogue de confirmation |
|||
await showDialog( |
|||
context: context, |
|||
builder: (context) => AlertDialog( |
|||
title: const Text('Commande Validée'), |
|||
content: const Text('Votre commande a été enregistrée avec succès.'), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () { |
|||
Navigator.pop(context); |
|||
// Réinitialiser le formulaire |
|||
_nomController.clear(); |
|||
_prenomController.clear(); |
|||
_emailController.clear(); |
|||
_telephoneController.clear(); |
|||
_adresseController.clear(); |
|||
setState(() { |
|||
_quantites.clear(); |
|||
_isLoading = false; |
|||
}); |
|||
}, |
|||
child: const Text('OK'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
|
|||
} catch (e) { |
|||
setState(() { |
|||
_isLoading = false; |
|||
}); |
|||
|
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Une erreur est survenue: ${e.toString()}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_nomController.dispose(); |
|||
_prenomController.dispose(); |
|||
_emailController.dispose(); |
|||
_telephoneController.dispose(); |
|||
_adresseController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue