Compare commits
4 Commits
master
...
31052025_0
| Author | SHA1 | Date |
|---|---|---|
|
|
c8fedd08e5 | 6 months ago |
|
|
9eafda610f | 6 months ago |
|
|
2bef06a2fe | 6 months ago |
|
|
57ea91b3d7 | 6 months ago |
45 changed files with 5827 additions and 2199 deletions
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,259 @@ |
|||||
|
import 'dart:io'; |
||||
|
import 'dart:ui'; |
||||
|
import 'package:flutter/foundation.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:mobile_scanner/mobile_scanner.dart'; |
||||
|
|
||||
|
class ScanQRPage extends StatefulWidget { |
||||
|
const ScanQRPage({super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<ScanQRPage> createState() => _ScanQRPageState(); |
||||
|
} |
||||
|
|
||||
|
class _ScanQRPageState extends State<ScanQRPage> { |
||||
|
MobileScannerController? cameraController; |
||||
|
bool _isScanComplete = false; |
||||
|
String? _scannedData; |
||||
|
bool _hasError = false; |
||||
|
String? _errorMessage; |
||||
|
bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS); |
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_initializeController(); |
||||
|
} |
||||
|
|
||||
|
void _initializeController() { |
||||
|
if (!isMobile) { |
||||
|
setState(() { |
||||
|
_hasError = true; |
||||
|
_errorMessage = "Le scanner QR n'est pas disponible sur cette plateforme."; |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
cameraController = MobileScannerController( |
||||
|
detectionSpeed: DetectionSpeed.noDuplicates, |
||||
|
facing: CameraFacing.back, |
||||
|
torchEnabled: false, |
||||
|
); |
||||
|
setState(() { |
||||
|
_hasError = false; |
||||
|
_errorMessage = null; |
||||
|
}); |
||||
|
} catch (e) { |
||||
|
setState(() { |
||||
|
_hasError = true; |
||||
|
_errorMessage = 'Erreur d\'initialisation de la caméra: $e'; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
cameraController?.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
appBar: AppBar( |
||||
|
title: const Text('Scanner QR Code'), |
||||
|
actions: _hasError ? [] : [ |
||||
|
if (cameraController != null) ...[ |
||||
|
IconButton( |
||||
|
color: Colors.white, |
||||
|
icon: const Icon(Icons.flash_on, color: Colors.white), |
||||
|
iconSize: 32.0, |
||||
|
onPressed: () => cameraController!.toggleTorch(), |
||||
|
), |
||||
|
IconButton( |
||||
|
color: Colors.white, |
||||
|
icon: const Icon(Icons.flip_camera_ios, color: Colors.white), |
||||
|
iconSize: 32.0, |
||||
|
onPressed: () => cameraController!.switchCamera(), |
||||
|
), |
||||
|
], |
||||
|
], |
||||
|
), |
||||
|
body: _hasError ? _buildErrorWidget() : _buildScannerWidget(), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildErrorWidget() { |
||||
|
return Center( |
||||
|
child: Padding( |
||||
|
padding: const EdgeInsets.all(16.0), |
||||
|
child: Column( |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
children: [ |
||||
|
const Icon( |
||||
|
Icons.error_outline, |
||||
|
size: 64, |
||||
|
color: Colors.red, |
||||
|
), |
||||
|
const SizedBox(height: 16), |
||||
|
const Text( |
||||
|
'Erreur de caméra', |
||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), |
||||
|
), |
||||
|
const SizedBox(height: 8), |
||||
|
Text( |
||||
|
_errorMessage ?? 'Une erreur s\'est produite', |
||||
|
textAlign: TextAlign.center, |
||||
|
style: const TextStyle(fontSize: 16), |
||||
|
), |
||||
|
const SizedBox(height: 24), |
||||
|
ElevatedButton( |
||||
|
onPressed: () { |
||||
|
_initializeController(); |
||||
|
}, |
||||
|
child: const Text('Réessayer'), |
||||
|
), |
||||
|
const SizedBox(height: 16), |
||||
|
const Text( |
||||
|
'Vérifiez que:\n• Le plugin mobile_scanner est installé\n• Les permissions de caméra sont accordées\n• Votre appareil a une caméra fonctionnelle', |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle(fontSize: 14, color: Colors.grey), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildScannerWidget() { |
||||
|
if (cameraController == null) { |
||||
|
return const Center(child: CircularProgressIndicator()); |
||||
|
} |
||||
|
|
||||
|
return 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!); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
errorBuilder: (context, error, child) { |
||||
|
return Center( |
||||
|
child: Column( |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
children: [ |
||||
|
const Icon(Icons.error, size: 64, color: Colors.red), |
||||
|
const SizedBox(height: 16), |
||||
|
Text('Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'), |
||||
|
const SizedBox(height: 16), |
||||
|
ElevatedButton( |
||||
|
onPressed: () => _initializeController(), |
||||
|
child: const Text('Réessayer'), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
), |
||||
|
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: SelectableText(data), // Permet de sélectionner le texte |
||||
|
actions: [ |
||||
|
TextButton( |
||||
|
onPressed: () { |
||||
|
Navigator.pop(context); |
||||
|
setState(() { |
||||
|
_isScanComplete = false; |
||||
|
}); |
||||
|
}, |
||||
|
child: const Text('Fermer'), |
||||
|
), |
||||
|
TextButton( |
||||
|
onPressed: () { |
||||
|
Navigator.pop(context); |
||||
|
Navigator.pop(context, data); // Retourner la donnée scannée |
||||
|
}, |
||||
|
child: const Text('Utiliser'), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
).then((_) { |
||||
|
setState(() { |
||||
|
_isScanComplete = false; |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
final Paint backgroundPaint = Paint() |
||||
|
..color = Colors.black.withOpacity(0.4); |
||||
|
canvas.drawRect(Rect.fromLTRB(0, 0, width, height), backgroundPaint); |
||||
|
|
||||
|
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, |
||||
|
); |
||||
|
|
||||
|
final Paint borderPaint = Paint() |
||||
|
..color = borderColor |
||||
|
..strokeWidth = borderWidth |
||||
|
..style = PaintingStyle.stroke; |
||||
|
|
||||
|
// Coins du scanner |
||||
|
_drawCorner(canvas, borderPaint, areaLeft, areaTop, borderLength, true, true); |
||||
|
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop, borderLength, false, true); |
||||
|
_drawCorner(canvas, borderPaint, areaLeft, areaTop + areaSize, borderLength, true, false); |
||||
|
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop + areaSize, borderLength, false, false); |
||||
|
} |
||||
|
|
||||
|
void _drawCorner(Canvas canvas, Paint paint, double x, double y, double length, bool isLeft, bool isTop) { |
||||
|
final double horizontalStart = isLeft ? x : x - length; |
||||
|
final double horizontalEnd = isLeft ? x + length : x; |
||||
|
final double verticalStart = isTop ? y : y - length; |
||||
|
final double verticalEnd = isTop ? y + length : y; |
||||
|
|
||||
|
canvas.drawLine(Offset(horizontalStart, y), Offset(horizontalEnd, y), paint); |
||||
|
canvas.drawLine(Offset(x, verticalStart), Offset(x, verticalEnd), paint); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
@ -1,31 +1,117 @@ |
|||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:youmazgestion/controller/userController.dart'; |
||||
|
|
||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { |
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { |
||||
final String title; |
final String title; |
||||
final Widget? subtitle; |
final Widget? subtitle; |
||||
|
final List<Widget>? actions; |
||||
|
final bool automaticallyImplyLeading; |
||||
|
final Color? backgroundColor; |
||||
|
|
||||
|
final UserController userController = Get.put(UserController()); |
||||
|
|
||||
const CustomAppBar({ |
CustomAppBar({ |
||||
Key? key, |
Key? key, |
||||
required this.title, |
required this.title, |
||||
this.subtitle, |
this.subtitle, |
||||
|
this.actions, |
||||
|
this.automaticallyImplyLeading = true, |
||||
|
this.backgroundColor, |
||||
}) : super(key: key); |
}) : super(key: key); |
||||
|
|
||||
@override |
@override |
||||
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0); |
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0); |
||||
|
|
||||
@override |
@override |
||||
Widget build(BuildContext context) { |
Widget build(BuildContext context) { |
||||
return AppBar( |
return Container( |
||||
title: subtitle == null |
decoration: BoxDecoration( |
||||
? Text(title) |
gradient: LinearGradient( |
||||
: Column( |
begin: Alignment.topLeft, |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
end: Alignment.bottomRight, |
||||
children: [ |
colors: [ |
||||
Text(title, style: TextStyle(fontSize: 20)), |
Colors.blue.shade900, |
||||
subtitle!, |
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 |
), |
||||
|
), |
||||
|
), |
||||
); |
); |
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
enum PaymentType { |
||||
|
cash, |
||||
|
card, |
||||
|
mvola, |
||||
|
orange, |
||||
|
airtel |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
class Pointage { |
||||
|
final int? id; |
||||
|
final String userName; |
||||
|
final String date; |
||||
|
final String heureArrivee; |
||||
|
final String heureDepart; |
||||
|
|
||||
|
Pointage({ |
||||
|
this.id, |
||||
|
required this.userName, |
||||
|
required this.date, |
||||
|
required this.heureArrivee, |
||||
|
required this.heureDepart, |
||||
|
}); |
||||
|
|
||||
|
// Pour SQLite |
||||
|
factory Pointage.fromMap(Map<String, dynamic> map) { |
||||
|
return Pointage( |
||||
|
id: map['id'], |
||||
|
userName: map['userName'] ?? '', |
||||
|
date: map['date'], |
||||
|
heureArrivee: map['heureArrivee'], |
||||
|
heureDepart: map['heureDepart'], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic> toMap() { |
||||
|
return { |
||||
|
'id': id, |
||||
|
'userName': userName, |
||||
|
'date': date, |
||||
|
'heureArrivee': heureArrivee, |
||||
|
'heureDepart': heureDepart, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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], |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,60 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'package:path/path.dart'; |
||||
|
import 'package:sqflite/sqflite.dart'; |
||||
|
import '../Models/pointage_model.dart'; |
||||
|
|
||||
|
class DatabaseHelper { |
||||
|
static final DatabaseHelper _instance = DatabaseHelper._internal(); |
||||
|
|
||||
|
factory DatabaseHelper() => _instance; |
||||
|
|
||||
|
DatabaseHelper._internal(); |
||||
|
|
||||
|
Database? _db; |
||||
|
|
||||
|
Future<Database> get database async { |
||||
|
if (_db != null) return _db!; |
||||
|
_db = await _initDatabase(); |
||||
|
return _db!; |
||||
|
} |
||||
|
|
||||
|
Future<Database> _initDatabase() async { |
||||
|
String databasesPath = await getDatabasesPath(); |
||||
|
String dbPath = join(databasesPath, 'pointage.db'); |
||||
|
return await openDatabase(dbPath, version: 1, onCreate: _onCreate); |
||||
|
} |
||||
|
|
||||
|
Future _onCreate(Database db, int version) async { |
||||
|
await db.execute(''' |
||||
|
CREATE TABLE pointages ( |
||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
userName TEXT NOT NULL, |
||||
|
date TEXT NOT NULL, |
||||
|
heureArrivee TEXT NOT NULL, |
||||
|
heureDepart TEXT NOT NULL |
||||
|
) |
||||
|
'''); |
||||
|
} |
||||
|
|
||||
|
Future<int> insertPointage(Pointage pointage) async { |
||||
|
final db = await database; |
||||
|
return await db.insert('pointages', pointage.toMap()); |
||||
|
} |
||||
|
|
||||
|
Future<List<Pointage>> getPointages() async { |
||||
|
final db = await database; |
||||
|
final pointages = await db.query('pointages'); |
||||
|
return pointages.map((pointage) => Pointage.fromMap(pointage)).toList(); |
||||
|
} |
||||
|
|
||||
|
Future<int> updatePointage(Pointage pointage) async { |
||||
|
final db = await database; |
||||
|
return await db.update('pointages', pointage.toMap(), |
||||
|
where: 'id = ?', whereArgs: [pointage.id]); |
||||
|
} |
||||
|
|
||||
|
Future<int> deletePointage(int id) async { |
||||
|
final db = await database; |
||||
|
return await db.delete('pointages', where: 'id = ?', whereArgs: [id]); |
||||
|
} |
||||
|
} |
||||
@ -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
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,123 +1,403 @@ |
|||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||
import 'package:youmazgestion/Components/app_bar.dart'; |
|
||||
import 'package:intl/intl.dart'; |
import 'package:intl/intl.dart'; |
||||
import '../Components/appDrawer.dart'; |
import 'package:youmazgestion/Models/client.dart'; |
||||
import '../controller/HistoryController.dart'; |
import 'package:youmazgestion/Models/produit.dart'; |
||||
import 'listCommandeHistory.dart'; |
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
||||
|
|
||||
|
class HistoriquePage extends StatefulWidget { |
||||
|
const HistoriquePage({super.key}); |
||||
|
|
||||
class HistoryPage extends GetView<HistoryController> { |
|
||||
@override |
@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 |
@override |
||||
Widget build(BuildContext context) { |
void initState() { |
||||
return Scaffold( |
super.initState(); |
||||
appBar: const CustomAppBar(title: 'Historique'), |
_loadCommandes(); |
||||
drawer: CustomDrawer(), |
} |
||||
body: Column( |
|
||||
children: [ |
Future<void> _loadCommandes() async { |
||||
Padding( |
setState(() { |
||||
padding: const EdgeInsets.all(16.0), |
_isLoading = true; |
||||
child: ElevatedButton( |
}); |
||||
onPressed: () { |
|
||||
controller.refreshOrders(); |
try { |
||||
controller.onInit(); |
final commandes = await _appDatabase.getCommandes(); |
||||
}, |
setState(() { |
||||
style: ElevatedButton.styleFrom( |
_commandes = commandes; |
||||
backgroundColor: Colors.deepOrangeAccent, |
_isLoading = false; |
||||
shape: RoundedRectangleBorder( |
}); |
||||
borderRadius: BorderRadius.circular(20.0), |
} 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, |
||||
|
), |
||||
), |
), |
||||
), |
IconButton( |
||||
child: const Text( |
icon: const Icon(Icons.close), |
||||
'Rafraîchir', |
onPressed: () => Get.back(), |
||||
style: TextStyle( |
|
||||
color: Colors.white, |
|
||||
fontSize: 16.0, |
|
||||
), |
), |
||||
|
], |
||||
|
), |
||||
|
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, |
||||
), |
), |
||||
), |
), |
||||
), |
const SizedBox(height: 16), |
||||
Expanded( |
const Text( |
||||
child: Obx( |
'Articles:', |
||||
() { |
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), |
||||
final distinctDates = controller.workDays; |
), |
||||
|
Expanded( |
||||
if (distinctDates.isEmpty) { |
child: ListView.builder( |
||||
return const Center( |
itemCount: details.length, |
||||
child: Text( |
itemBuilder: (context, index) { |
||||
'Aucune journée de travail trouvée', |
final detail = details[index]; |
||||
style: TextStyle( |
return ListTile( |
||||
fontSize: 18.0, |
leading: const Icon(Icons.shopping_bag), |
||||
fontWeight: FontWeight.bold, |
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 { |
try { |
||||
final parsedDate = DateFormat('dd-MM-yyyy').parse(date); |
await _appDatabase.updateStatutCommande( |
||||
print('parsedDate1: $parsedDate'); |
commandeId, StatutCommande.livree); |
||||
final formattedDate = DateFormat('yyyy-MM-dd').format(parsedDate); |
Get.back(); // Ferme le bottom sheet |
||||
print('formattedDate1: $formattedDate'); |
_loadCommandes(); |
||||
return formattedDate; |
Get.snackbar( |
||||
|
'Succès', |
||||
|
'Statut de la commande mis à jour', |
||||
|
snackPosition: SnackPosition.BOTTOM, |
||||
|
backgroundColor: Colors.green, |
||||
|
colorText: Colors.white, |
||||
|
); |
||||
} catch (e) { |
} catch (e) { |
||||
print('Error parsing date: $date'); |
Get.snackbar( |
||||
return ''; |
'Erreur', |
||||
|
'Impossible de mettre à jour le statut: ${e.toString()}', |
||||
|
snackPosition: SnackPosition.BOTTOM, |
||||
|
backgroundColor: Colors.red, |
||||
|
colorText: Colors.white, |
||||
|
); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
// transformer string en DateTime |
@override |
||||
void navigateToDetailPage(String selectedDate) { |
Widget build(BuildContext context) { |
||||
print('selectedDate: $selectedDate'); |
return Scaffold( |
||||
DateTime parsedDate = DateFormat('yyyy-MM-dd').parse(selectedDate); |
appBar: AppBar( |
||||
print('parsedDate: $parsedDate'); |
title: const Text('Historique des Commandes'), |
||||
|
actions: [ |
||||
Get.to(() => HistoryDetailPage(selectedDate: parsedDate)); |
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,851 @@ |
|||||
|
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: Row( |
||||
|
children: [ |
||||
|
Container( |
||||
|
padding: const EdgeInsets.all(8), |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.blue.shade100, |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
), |
||||
|
child: Icon(Icons.person_add, color: Colors.blue.shade700), |
||||
|
), |
||||
|
const SizedBox(width: 12), |
||||
|
const Text('Informations Client'), |
||||
|
], |
||||
|
), |
||||
|
content: Container( |
||||
|
width: 600, |
||||
|
constraints: const BoxConstraints(maxHeight: 600), |
||||
|
child: SingleChildScrollView( |
||||
|
child: Form( |
||||
|
key: _formKey, |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
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, |
||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), |
||||
|
), |
||||
|
onPressed: () { |
||||
|
if (_formKey.currentState!.validate()) { |
||||
|
Get.back(); |
||||
|
// Au lieu d'afficher juste un message, on valide directement la commande |
||||
|
_submitOrder(); |
||||
|
} |
||||
|
}, |
||||
|
child: const Text('Valider la commande'), // Changement de texte ici |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
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)} MGA', |
||||
|
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)} MGA'), |
||||
|
trailing: Text( |
||||
|
'${(entry.value * product.price).toStringAsFixed(2)} MGA', |
||||
|
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)} MGA', |
||||
|
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 { |
||||
|
// Vérifier d'abord si le panier est vide |
||||
|
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, |
||||
|
); |
||||
|
_showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Ensuite vérifier les informations client |
||||
|
if (_nomController.text.isEmpty || |
||||
|
_prenomController.text.isEmpty || |
||||
|
_emailController.text.isEmpty || |
||||
|
_telephoneController.text.isEmpty || |
||||
|
_adresseController.text.isEmpty) { |
||||
|
Get.snackbar( |
||||
|
'Informations manquantes', |
||||
|
'Veuillez remplir les informations client', |
||||
|
snackPosition: SnackPosition.BOTTOM, |
||||
|
backgroundColor: Colors.red, |
||||
|
colorText: Colors.white, |
||||
|
); |
||||
|
_showClientFormDialog(); |
||||
|
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); |
||||
|
|
||||
|
// 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 et expédié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
@ -0,0 +1,190 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:youmazgestion/Services/pointageDatabase.dart'; |
||||
|
import 'package:youmazgestion/Models/pointage_model.dart'; |
||||
|
|
||||
|
class PointagePage extends StatefulWidget { |
||||
|
const PointagePage({Key? key}) : super(key: key); |
||||
|
|
||||
|
@override |
||||
|
State<PointagePage> createState() => _PointagePageState(); |
||||
|
} |
||||
|
|
||||
|
class _PointagePageState extends State<PointagePage> { |
||||
|
final DatabaseHelper _databaseHelper = DatabaseHelper(); |
||||
|
List<Pointage> _pointages = []; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_loadPointages(); |
||||
|
} |
||||
|
|
||||
|
Future<void> _loadPointages() async { |
||||
|
final pointages = await _databaseHelper.getPointages(); |
||||
|
setState(() { |
||||
|
_pointages = pointages; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Future<void> _showAddDialog() async { |
||||
|
final _arrivalController = TextEditingController(); |
||||
|
|
||||
|
await showDialog( |
||||
|
context: context, |
||||
|
builder: (context) { |
||||
|
return AlertDialog( |
||||
|
title: Text('Ajouter Pointage'), |
||||
|
content: TextField( |
||||
|
controller: _arrivalController, |
||||
|
decoration: InputDecoration( |
||||
|
labelText: 'Heure d\'arrivée (HH:mm)', |
||||
|
), |
||||
|
), |
||||
|
actions: [ |
||||
|
TextButton( |
||||
|
child: Text('Annuler'), |
||||
|
onPressed: () => Navigator.of(context).pop(), |
||||
|
), |
||||
|
ElevatedButton( |
||||
|
child: Text('Ajouter'), |
||||
|
onPressed: () async { |
||||
|
final pointage = Pointage( |
||||
|
userName: |
||||
|
"Nom de l'utilisateur", // fixed value, customize if needed |
||||
|
date: DateTime.now().toString().split(' ')[0], |
||||
|
heureArrivee: _arrivalController.text, |
||||
|
heureDepart: '', |
||||
|
); |
||||
|
await _databaseHelper.insertPointage(pointage); |
||||
|
Navigator.of(context).pop(); |
||||
|
_loadPointages(); |
||||
|
}, |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _scanQRCode({required bool isEntree}) { |
||||
|
// Ici tu peux intégrer ton scanner QR. |
||||
|
ScaffoldMessenger.of(context).showSnackBar( |
||||
|
SnackBar( |
||||
|
content: Text(isEntree ? "Scan QR pour Entrée" : "Scan QR pour Sortie"), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
appBar: AppBar( |
||||
|
title: const Text('Pointage'), |
||||
|
), |
||||
|
body: _pointages.isEmpty |
||||
|
? Center(child: Text('Aucun pointage enregistré.')) |
||||
|
: ListView.builder( |
||||
|
itemCount: _pointages.length, |
||||
|
itemBuilder: (context, index) { |
||||
|
final pointage = _pointages[index]; |
||||
|
return Padding( |
||||
|
padding: const EdgeInsets.symmetric( |
||||
|
horizontal: 12.0, vertical: 6.0), |
||||
|
child: Card( |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(16), |
||||
|
side: BorderSide(color: Colors.blueGrey.shade100), |
||||
|
), |
||||
|
elevation: 4, |
||||
|
shadowColor: Colors.blueGrey.shade50, |
||||
|
child: Padding( |
||||
|
padding: const EdgeInsets.symmetric( |
||||
|
horizontal: 8.0, vertical: 4), |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Row( |
||||
|
children: [ |
||||
|
CircleAvatar( |
||||
|
backgroundColor: Colors.blue.shade100, |
||||
|
child: Icon(Icons.person, color: Colors.blue), |
||||
|
), |
||||
|
const SizedBox(width: 10), |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
pointage |
||||
|
.userName, // suppose non-null (corrige si null possible) |
||||
|
style: TextStyle( |
||||
|
fontWeight: FontWeight.bold, |
||||
|
fontSize: 18), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
Divider(), |
||||
|
Text( |
||||
|
pointage.date, |
||||
|
style: const TextStyle( |
||||
|
color: Colors.black87, fontSize: 15), |
||||
|
), |
||||
|
Row( |
||||
|
children: [ |
||||
|
Icon(Icons.login, |
||||
|
size: 18, color: Colors.green.shade700), |
||||
|
const SizedBox(width: 6), |
||||
|
Text("Arrivée : ${pointage.heureArrivee}", |
||||
|
style: |
||||
|
TextStyle(color: Colors.green.shade700)), |
||||
|
], |
||||
|
), |
||||
|
Row( |
||||
|
children: [ |
||||
|
Icon(Icons.logout, |
||||
|
size: 18, color: Colors.red.shade700), |
||||
|
const SizedBox(width: 6), |
||||
|
Text( |
||||
|
"Départ : ${pointage.heureDepart.isNotEmpty ? pointage.heureDepart : "---"}", |
||||
|
style: TextStyle(color: Colors.red.shade700)), |
||||
|
], |
||||
|
), |
||||
|
const SizedBox(height: 6), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
), |
||||
|
floatingActionButton: Column( |
||||
|
mainAxisAlignment: MainAxisAlignment.end, |
||||
|
crossAxisAlignment: CrossAxisAlignment.end, |
||||
|
children: [ |
||||
|
FloatingActionButton.extended( |
||||
|
onPressed: () => _scanQRCode(isEntree: true), |
||||
|
label: Text('Entrée'), |
||||
|
icon: Icon(Icons.qr_code_scanner, color: Colors.green), |
||||
|
backgroundColor: Colors.white, |
||||
|
foregroundColor: Colors.green, |
||||
|
heroTag: 'btnEntree', |
||||
|
), |
||||
|
SizedBox(height: 12), |
||||
|
FloatingActionButton.extended( |
||||
|
onPressed: () => _scanQRCode(isEntree: false), |
||||
|
label: Text('Sortie'), |
||||
|
icon: Icon(Icons.qr_code_scanner, color: Colors.red), |
||||
|
backgroundColor: Colors.white, |
||||
|
foregroundColor: Colors.red, |
||||
|
heroTag: 'btnSortie', |
||||
|
), |
||||
|
SizedBox(height: 12), |
||||
|
FloatingActionButton( |
||||
|
onPressed: _showAddDialog, |
||||
|
tooltip: 'Ajouter Pointage', |
||||
|
child: const Icon(Icons.add), |
||||
|
heroTag: 'btnAdd', |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue