Compare commits
7 Commits
31052025_0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 25abc65480 | |||
| beb2048a7a | |||
| b1d176aa2a | |||
| 8d86d99e24 | |||
| 5f665acf35 | |||
| 38af810b79 | |||
| da03076411 |
@ -1,6 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.FLASHLIGHT" />
|
|
||||||
<application
|
<application
|
||||||
android:label="my_app"
|
android:label="my_app"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@ -14,7 +12,6 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
BIN
assets/mvola.jpg
BIN
assets/mvola.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB |
@ -2,8 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Cette application a besoin d'accéder à la caméra pour scanner les codes QR</string>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@ -49,6 +47,5 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,259 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Views/Dashboard.dart';
|
|
||||||
import 'package:youmazgestion/Views/HandleProduct.dart';
|
import 'package:youmazgestion/Views/HandleProduct.dart';
|
||||||
import 'package:youmazgestion/Views/RoleListPage.dart';
|
import 'package:youmazgestion/Views/RoleListPage.dart';
|
||||||
import 'package:youmazgestion/Views/commandManagement.dart';
|
import 'package:youmazgestion/Views/commandManagement.dart';
|
||||||
@ -106,7 +105,7 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
permissionAction: 'view',
|
permissionAction: 'view',
|
||||||
permissionRoute: '/accueil',
|
permissionRoute: '/accueil',
|
||||||
onTap: () => Get.to( DashboardPage()),
|
onTap: () => Get.to(const AccueilPage()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -229,11 +228,11 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
List<Widget> rapportsItems = [
|
List<Widget> rapportsItems = [
|
||||||
await _buildDrawerItem(
|
await _buildDrawerItem(
|
||||||
icon: Icons.bar_chart,
|
icon: Icons.bar_chart,
|
||||||
title: "Bilan ",
|
title: "Bilan mensuel",
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
permissionAction: 'read',
|
permissionAction: 'read',
|
||||||
permissionRoute: '/bilan',
|
permissionRoute: '/bilan',
|
||||||
onTap: () => Get.to( DashboardPage()),
|
onTap: () => Get.to(const BilanMois()),
|
||||||
),
|
),
|
||||||
await _buildDrawerItem(
|
await _buildDrawerItem(
|
||||||
icon: Icons.history,
|
icon: Icons.history,
|
||||||
@ -292,7 +291,6 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
|
|
||||||
drawerItems.add(const Divider());
|
drawerItems.add(const Divider());
|
||||||
|
|
||||||
|
|
||||||
drawerItems.add(
|
drawerItems.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout, color: Colors.red),
|
leading: const Icon(Icons.logout, color: Colors.red),
|
||||||
@ -351,6 +349,3 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryPage {
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,117 +1,31 @@
|
|||||||
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 : 80.0);
|
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return AppBar(
|
||||||
decoration: BoxDecoration(
|
title: subtitle == null
|
||||||
gradient: LinearGradient(
|
? Text(title)
|
||||||
begin: Alignment.topLeft,
|
: Column(
|
||||||
end: Alignment.bottomRight,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
colors: [
|
children: [
|
||||||
Colors.blue.shade900,
|
Text(title, style: TextStyle(fontSize: 20)),
|
||||||
Colors.blue.shade800,
|
subtitle!,
|
||||||
],
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue.shade900.withOpacity(0.3),
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
blurRadius: 4,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: backgroundColor ?? Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
|
||||||
centerTitle: false,
|
|
||||||
iconTheme: const IconThemeData(
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
actions: actions,
|
|
||||||
title: subtitle == null
|
|
||||||
? Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Obx(() => Text(
|
|
||||||
userController.role!='Super Admin'?'Point de vente: ${userController.pointDeVenteDesignation}':'',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
color: Colors.white.withOpacity(0.9),
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
if (subtitle != null) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
DefaultTextStyle(
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white.withOpacity(0.8),
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
child: subtitle!,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
flexibleSpace: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
Colors.blue.shade900,
|
|
||||||
Colors.blue.shade800,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
// autres propriétés si besoin
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +0,0 @@
|
|||||||
enum PaymentType {
|
|
||||||
cash,
|
|
||||||
card,
|
|
||||||
mvola,
|
|
||||||
orange,
|
|
||||||
airtel
|
|
||||||
}
|
|
||||||
@ -49,6 +49,7 @@ class Client {
|
|||||||
String get nomComplet => '$prenom $nom';
|
String get nomComplet => '$prenom $nom';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Models/commande.dart
|
||||||
enum StatutCommande {
|
enum StatutCommande {
|
||||||
enAttente,
|
enAttente,
|
||||||
confirmee,
|
confirmee,
|
||||||
@ -66,8 +67,6 @@ class Commande {
|
|||||||
final double montantTotal;
|
final double montantTotal;
|
||||||
final String? notes;
|
final String? notes;
|
||||||
final DateTime? dateLivraison;
|
final DateTime? dateLivraison;
|
||||||
final int? commandeurId;
|
|
||||||
final int? validateurId;
|
|
||||||
|
|
||||||
// Données du client (pour les jointures)
|
// Données du client (pour les jointures)
|
||||||
final String? clientNom;
|
final String? clientNom;
|
||||||
@ -82,8 +81,6 @@ class Commande {
|
|||||||
required this.montantTotal,
|
required this.montantTotal,
|
||||||
this.notes,
|
this.notes,
|
||||||
this.dateLivraison,
|
this.dateLivraison,
|
||||||
this.commandeurId,
|
|
||||||
this.validateurId,
|
|
||||||
this.clientNom,
|
this.clientNom,
|
||||||
this.clientPrenom,
|
this.clientPrenom,
|
||||||
this.clientEmail,
|
this.clientEmail,
|
||||||
@ -98,8 +95,6 @@ class Commande {
|
|||||||
'montantTotal': montantTotal,
|
'montantTotal': montantTotal,
|
||||||
'notes': notes,
|
'notes': notes,
|
||||||
'dateLivraison': dateLivraison?.toIso8601String(),
|
'dateLivraison': dateLivraison?.toIso8601String(),
|
||||||
'commandeurId': commandeurId,
|
|
||||||
'validateurId': validateurId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,8 +109,6 @@ class Commande {
|
|||||||
dateLivraison: map['dateLivraison'] != null
|
dateLivraison: map['dateLivraison'] != null
|
||||||
? DateTime.parse(map['dateLivraison'])
|
? DateTime.parse(map['dateLivraison'])
|
||||||
: null,
|
: null,
|
||||||
commandeurId: map['commandeurId'],
|
|
||||||
validateurId: map['validateurId'],
|
|
||||||
clientNom: map['clientNom'],
|
clientNom: map['clientNom'],
|
||||||
clientPrenom: map['clientPrenom'],
|
clientPrenom: map['clientPrenom'],
|
||||||
clientEmail: map['clientEmail'],
|
clientEmail: map['clientEmail'],
|
||||||
@ -136,8 +129,6 @@ class Commande {
|
|||||||
return 'Livrée';
|
return 'Livrée';
|
||||||
case StatutCommande.annulee:
|
case StatutCommande.annulee:
|
||||||
return 'Annulée';
|
return 'Annulée';
|
||||||
default:
|
|
||||||
return 'Inconnu';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +138,6 @@ class Commande {
|
|||||||
: 'Client inconnu';
|
: 'Client inconnu';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Models/detail_commande.dart
|
// Models/detail_commande.dart
|
||||||
class DetailCommande {
|
class DetailCommande {
|
||||||
final int? id;
|
final int? id;
|
||||||
|
|||||||
@ -8,7 +8,6 @@ class Product {
|
|||||||
final String? description;
|
final String? description;
|
||||||
String? qrCode;
|
String? qrCode;
|
||||||
final String? reference;
|
final String? reference;
|
||||||
final int? pointDeVenteId;
|
|
||||||
|
|
||||||
Product({
|
Product({
|
||||||
this.id,
|
this.id,
|
||||||
@ -20,7 +19,6 @@ class Product {
|
|||||||
this.description = '',
|
this.description = '',
|
||||||
this.qrCode,
|
this.qrCode,
|
||||||
this.reference,
|
this.reference,
|
||||||
this.pointDeVenteId
|
|
||||||
});
|
});
|
||||||
// Vérifie si le stock est défini
|
// Vérifie si le stock est défini
|
||||||
bool isStockDefined() {
|
bool isStockDefined() {
|
||||||
@ -42,7 +40,6 @@ class Product {
|
|||||||
'description': description ?? '',
|
'description': description ?? '',
|
||||||
'qrCode': qrCode ?? '',
|
'qrCode': qrCode ?? '',
|
||||||
'reference': reference ?? '',
|
'reference': reference ?? '',
|
||||||
'point_de_vente_id':pointDeVenteId
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +54,6 @@ class Product {
|
|||||||
description: map['description'],
|
description: map['description'],
|
||||||
qrCode: map['qrCode'],
|
qrCode: map['qrCode'],
|
||||||
reference: map['reference'],
|
reference: map['reference'],
|
||||||
pointDeVenteId : map['point_de_vente_id']
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,7 +7,6 @@ class Users {
|
|||||||
String username;
|
String username;
|
||||||
int roleId;
|
int roleId;
|
||||||
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN
|
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN
|
||||||
int? pointDeVenteId;
|
|
||||||
|
|
||||||
Users({
|
Users({
|
||||||
this.id,
|
this.id,
|
||||||
@ -18,7 +17,6 @@ class Users {
|
|||||||
required this.username,
|
required this.username,
|
||||||
required this.roleId,
|
required this.roleId,
|
||||||
this.roleName,
|
this.roleName,
|
||||||
this.pointDeVenteId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
@ -29,7 +27,6 @@ class Users {
|
|||||||
'password': password,
|
'password': password,
|
||||||
'username': username,
|
'username': username,
|
||||||
'role_id': roleId,
|
'role_id': roleId,
|
||||||
'point_de_vente_id' : pointDeVenteId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,11 +46,9 @@ class Users {
|
|||||||
username: map['username'],
|
username: map['username'],
|
||||||
roleId: map['role_id'],
|
roleId: map['role_id'],
|
||||||
roleName: map['role_name'], // Depuis les requêtes avec JOIN
|
roleName: map['role_name'], // Depuis les requêtes avec JOIN
|
||||||
pointDeVenteId : map['point_de_vente_id']
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter pour la compatibilité avec l'ancien code
|
// Getter pour la compatibilité avec l'ancien code
|
||||||
String get role => roleName ?? '';
|
String get role => roleName ?? '';
|
||||||
|
|
||||||
}
|
}
|
||||||
728
lib/Services/app_database.dart
Normal file
728
lib/Services/app_database.dart
Normal file
@ -0,0 +1,728 @@
|
|||||||
|
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'},
|
||||||
|
{'name': 'Gérer les pointages', 'route': '/pointage'},
|
||||||
|
];
|
||||||
|
|
||||||
|
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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
559
lib/Services/productDatabase.dart
Normal file
559
lib/Services/productDatabase.dart
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
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
Load Diff
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,10 @@ import 'package:qr_flutter/qr_flutter.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:excel/excel.dart' hide Border;
|
import 'package:excel/excel.dart' hide Border;
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import '../Components/appDrawer.dart';
|
import '../Components/appDrawer.dart';
|
||||||
import '../Components/app_bar.dart';
|
import '../Components/app_bar.dart';
|
||||||
import '../Models/produit.dart';
|
import '../Models/produit.dart';
|
||||||
//import '../Services/productDatabase.dart';
|
import '../Services/productDatabase.dart';
|
||||||
|
|
||||||
|
|
||||||
class ProductManagementPage extends StatefulWidget {
|
class ProductManagementPage extends StatefulWidget {
|
||||||
@ -25,7 +24,7 @@ class ProductManagementPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ProductManagementPageState extends State<ProductManagementPage> {
|
class _ProductManagementPageState extends State<ProductManagementPage> {
|
||||||
final AppDatabase _productDatabase = AppDatabase.instance;
|
final ProductDatabase _productDatabase = ProductDatabase.instance;
|
||||||
List<Product> _products = [];
|
List<Product> _products = [];
|
||||||
List<Product> _filteredProducts = [];
|
List<Product> _filteredProducts = [];
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
@ -1042,7 +1041,7 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
|
|||||||
return pw.Center(
|
return pw.Center(
|
||||||
child: pw.Column(
|
child: pw.Column(
|
||||||
children: [
|
children: [
|
||||||
// pw.Text('QR Code - ${product.name}', style: pw.TextStyle(fontSize: 20)),
|
pw.Text('QR Code - ${product.name}', style: pw.TextStyle(fontSize: 20)),
|
||||||
pw.SizedBox(height: 20),
|
pw.SizedBox(height: 20),
|
||||||
pw.BarcodeWidget(
|
pw.BarcodeWidget(
|
||||||
barcode: pw.Barcode.qrCode(),
|
barcode: pw.Barcode.qrCode(),
|
||||||
@ -1412,7 +1411,7 @@ Future<void> _generatePDF(Product product, String qrUrl) async {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Gestion des produits'),
|
appBar: const CustomAppBar(title: 'Gestion des produits'),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
floatingActionButton: Column(
|
floatingActionButton: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
//import 'package:youmazgestion/Models/Permission.dart';
|
import 'package:youmazgestion/Models/Permission.dart';
|
||||||
//import 'package:youmazgestion/Services/app_database.dart';
|
import 'package:youmazgestion/Services/app_database.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import 'package:youmazgestion/Views/RolePermissionPage.dart';
|
import 'package:youmazgestion/Views/RolePermissionPage.dart';
|
||||||
|
|
||||||
class RoleListPage extends StatefulWidget {
|
class RoleListPage extends StatefulWidget {
|
||||||
@ -48,7 +47,7 @@ class _RoleListPageState extends State<RoleListPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: "Gestion des rôles"),
|
appBar: const CustomAppBar(title: "Gestion des rôles"),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Models/Permission.dart';
|
import 'package:youmazgestion/Models/Permission.dart';
|
||||||
|
import 'package:youmazgestion/Services/app_database.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
|
|
||||||
class RolePermissionsPage extends StatefulWidget {
|
class RolePermissionsPage extends StatefulWidget {
|
||||||
final Role role;
|
final Role role;
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class _BilanMoisState extends State<BilanMois> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Bilan du mois'),
|
appBar: const CustomAppBar(title: 'Bilan du mois'),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Les 3 cartes en haut
|
// Les 3 cartes en haut
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -12,8 +11,7 @@ import 'package:open_file/open_file.dart';
|
|||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||||
import 'package:youmazgestion/Models/client.dart';
|
import 'package:youmazgestion/Models/client.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/productDatabase.dart';
|
||||||
import 'package:youmazgestion/controller/userController.dart';
|
|
||||||
|
|
||||||
class GestionCommandesPage extends StatefulWidget {
|
class GestionCommandesPage extends StatefulWidget {
|
||||||
const GestionCommandesPage({super.key});
|
const GestionCommandesPage({super.key});
|
||||||
@ -23,14 +21,14 @@ class GestionCommandesPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
||||||
final AppDatabase _database = AppDatabase.instance;
|
final ProductDatabase _database = ProductDatabase.instance;
|
||||||
List<Commande> _commandes = [];
|
List<Commande> _commandes = [];
|
||||||
List<Commande> _filteredCommandes = [];
|
List<Commande> _filteredCommandes = [];
|
||||||
StatutCommande? _selectedStatut;
|
StatutCommande? _selectedStatut;
|
||||||
DateTime? _selectedDate;
|
DateTime? _selectedDate;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
bool _showCancelledOrders = false;
|
bool _showCancelledOrders =
|
||||||
final userController = Get.find<UserController>();
|
false; // Nouveau: contrôle l'affichage des commandes annulées
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -65,189 +63,62 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
DateFormat('yyyy-MM-dd').format(commande.dateCommande) ==
|
DateFormat('yyyy-MM-dd').format(commande.dateCommande) ==
|
||||||
DateFormat('yyyy-MM-dd').format(_selectedDate!);
|
DateFormat('yyyy-MM-dd').format(_selectedDate!);
|
||||||
|
|
||||||
final shouldShowCancelled = _showCancelledOrders || commande.statut != StatutCommande.annulee;
|
// Nouveau: filtrer les commandes annulées selon le toggle
|
||||||
|
final shouldShowCancelled =
|
||||||
|
_showCancelledOrders || commande.statut != StatutCommande.annulee;
|
||||||
|
|
||||||
return matchesSearch && matchesStatut && matchesDate && shouldShowCancelled;
|
return matchesSearch &&
|
||||||
|
matchesStatut &&
|
||||||
|
matchesDate &&
|
||||||
|
shouldShowCancelled;
|
||||||
}).toList();
|
}).toList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateStatut(int commandeId, StatutCommande newStatut, {int? validateurId}) async {
|
Future<void> _updateStatut(int commandeId, StatutCommande newStatut) async {
|
||||||
// D'abord récupérer la commande existante pour avoir toutes ses valeurs
|
|
||||||
final commandeExistante = await _database.getCommandeById(commandeId);
|
|
||||||
|
|
||||||
if (commandeExistante == null) {
|
|
||||||
Get.snackbar(
|
|
||||||
'Erreur',
|
|
||||||
'Commande introuvable',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateurId != null) {
|
|
||||||
// Mise à jour avec validateur
|
|
||||||
await _database.updateCommande(Commande(
|
|
||||||
id: commandeId,
|
|
||||||
clientId: commandeExistante.clientId,
|
|
||||||
dateCommande: commandeExistante.dateCommande,
|
|
||||||
statut: newStatut,
|
|
||||||
montantTotal: commandeExistante.montantTotal,
|
|
||||||
notes: commandeExistante.notes,
|
|
||||||
dateLivraison: commandeExistante.dateLivraison,
|
|
||||||
commandeurId: commandeExistante.commandeurId,
|
|
||||||
validateurId: validateurId, // On met à jour le validateur
|
|
||||||
clientNom: commandeExistante.clientNom,
|
|
||||||
clientPrenom: commandeExistante.clientPrenom,
|
|
||||||
clientEmail: commandeExistante.clientEmail,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
// Mise à jour simple du statut
|
|
||||||
await _database.updateStatutCommande(commandeId, newStatut);
|
await _database.updateStatutCommande(commandeId, newStatut);
|
||||||
}
|
await _loadCommandes();
|
||||||
|
|
||||||
await _loadCommandes();
|
// Amélioration: message plus spécifique selon le statut
|
||||||
|
String message = 'Statut de la commande mis à jour';
|
||||||
|
Color backgroundColor = Colors.green;
|
||||||
|
|
||||||
String message = 'Statut de la commande mis à jour';
|
switch (newStatut) {
|
||||||
Color backgroundColor = Colors.green;
|
case StatutCommande.annulee:
|
||||||
|
message = 'Commande annulée avec succès';
|
||||||
switch (newStatut) {
|
backgroundColor = Colors.orange;
|
||||||
case StatutCommande.annulee:
|
break;
|
||||||
message = 'Commande annulée avec succès';
|
case StatutCommande.livree:
|
||||||
backgroundColor = Colors.orange;
|
message = 'Commande marquée comme livrée';
|
||||||
break;
|
backgroundColor = Colors.green;
|
||||||
case StatutCommande.livree:
|
break;
|
||||||
message = 'Commande marquée comme livrée';
|
case StatutCommande.confirmee:
|
||||||
backgroundColor = Colors.green;
|
message = 'Commande confirmée';
|
||||||
break;
|
backgroundColor = Colors.blue;
|
||||||
case StatutCommande.confirmee:
|
break;
|
||||||
message = 'Commande confirmée';
|
default:
|
||||||
backgroundColor = Colors.blue;
|
break;
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.snackbar(
|
|
||||||
'Succès',
|
|
||||||
message,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
colorText: Colors.white,
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showPaymentOptions(Commande commande) async {
|
|
||||||
final selectedPayment = await showDialog<PaymentMethod>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PaymentMethodDialog(commande: commande),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedPayment != null) {
|
|
||||||
if (selectedPayment.type == PaymentType.cash) {
|
|
||||||
await _showCashPaymentDialog(commande, selectedPayment.amountGiven);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirmer la commande avec le validateur actuel
|
|
||||||
await _updateStatut(
|
|
||||||
commande.id!,
|
|
||||||
StatutCommande.confirmee,
|
|
||||||
validateurId: userController.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Générer le ticket de caisse
|
|
||||||
await _generateReceipt(commande, selectedPayment);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showCashPaymentDialog(Commande commande, double amountGiven) async {
|
Get.snackbar(
|
||||||
final amountController = TextEditingController(
|
'Succès',
|
||||||
text: amountGiven.toStringAsFixed(2),
|
message,
|
||||||
);
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
await showDialog(
|
colorText: Colors.white,
|
||||||
context: context,
|
duration: const Duration(seconds: 2),
|
||||||
builder: (context) {
|
|
||||||
final change = amountGiven - commande.montantTotal;
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Paiement en liquide'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('Montant total: ${commande.montantTotal.toStringAsFixed(2)} MGA'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextField(
|
|
||||||
controller: amountController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Montant donné',
|
|
||||||
prefixText: 'MGA ',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
onChanged: (value) {
|
|
||||||
final newAmount = double.tryParse(value) ?? 0;
|
|
||||||
if (newAmount >= commande.montantTotal) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
if (amountGiven >= commande.montantTotal)
|
|
||||||
Text(
|
|
||||||
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (amountGiven < commande.montantTotal)
|
|
||||||
Text(
|
|
||||||
'Montant insuffisant',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('Annuler'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: const Text('Valider'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _generateInvoice(Commande commande) async {
|
Future<void> _generateInvoice(Commande commande) async {
|
||||||
final details = await _database.getDetailsCommande(commande.id!);
|
final details = await _database.getDetailsCommande(commande.id!);
|
||||||
final client = await _database.getClientById(commande.clientId);
|
final client = await _database.getClientById(commande.clientId);
|
||||||
final commandeur = commande.commandeurId != null
|
|
||||||
? await _database.getUserById(commande.commandeurId!)
|
|
||||||
: null;
|
|
||||||
final validateur = commande.validateurId != null
|
|
||||||
? await _database.getUserById(commande.validateurId!)
|
|
||||||
: null;
|
|
||||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
|
||||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final pdf = pw.Document();
|
final pdf = pw.Document();
|
||||||
final imageBytes = await loadImage();
|
final imageBytes = await loadImage(); // Charge les données de l'image
|
||||||
final image = pw.MemoryImage(imageBytes);
|
|
||||||
|
|
||||||
|
final image = pw.MemoryImage(imageBytes);
|
||||||
|
// Amélioration: styles plus professionnels
|
||||||
final headerStyle = pw.TextStyle(
|
final headerStyle = pw.TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: pw.FontWeight.bold,
|
fontWeight: pw.FontWeight.bold,
|
||||||
@ -264,6 +135,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
color: PdfColors.grey600,
|
color: PdfColors.grey600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Contenu du PDF amélioré
|
||||||
pdf.addPage(
|
pdf.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
margin: const pw.EdgeInsets.all(20),
|
margin: const pw.EdgeInsets.all(20),
|
||||||
@ -271,6 +143,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
return pw.Column(
|
return pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// En-tête avec logo (si disponible)
|
||||||
pw.Row(
|
pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
@ -278,6 +151,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
pw.Column(
|
pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Placeholder pour le logo - à remplacer par votre logo
|
||||||
pw.Container(
|
pw.Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 80,
|
height: 80,
|
||||||
@ -290,9 +164,10 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
),
|
),
|
||||||
pw.SizedBox(height: 10),
|
pw.SizedBox(height: 10),
|
||||||
pw.Text('guycom', style: headerStyle),
|
pw.Text('guycom', style: headerStyle),
|
||||||
if (pointDeVente != null)
|
pw.Text('123 Rue des Entreprises', style: subtitleStyle),
|
||||||
pw.Text('Point de vente: ${pointDeVente['designation']}', style: subtitleStyle),
|
pw.Text('Antananarivo, Madagascar', style: subtitleStyle),
|
||||||
pw.Text('Tél: +213 123 456 789', style: subtitleStyle),
|
pw.Text('Tél: +213 123 456 789', style: subtitleStyle),
|
||||||
|
pw.Text('Site: guycom.mg', style: subtitleStyle),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.Column(
|
pw.Column(
|
||||||
@ -352,33 +227,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
pw.SizedBox(height: 20),
|
pw.SizedBox(height: 30),
|
||||||
|
|
||||||
// Informations personnel
|
|
||||||
if (commandeur != null || validateur != null)
|
|
||||||
pw.Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const pw.EdgeInsets.all(12),
|
|
||||||
decoration: pw.BoxDecoration(
|
|
||||||
color: PdfColors.grey100,
|
|
||||||
borderRadius: pw.BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: pw.Column(
|
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
pw.Text('PERSONNEL:', style: titleStyle),
|
|
||||||
pw.SizedBox(height: 5),
|
|
||||||
if (commandeur != null)
|
|
||||||
pw.Text('Commandeur: ${commandeur.name} ',
|
|
||||||
style: pw.TextStyle(fontSize: 12)),
|
|
||||||
if (validateur != null)
|
|
||||||
pw.Text('Validateur: ${validateur.name}',
|
|
||||||
style: pw.TextStyle(fontSize: 12)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
pw.SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Tableau des produits
|
// Tableau des produits
|
||||||
pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle),
|
pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle),
|
||||||
@ -414,8 +263,10 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
|
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
|
||||||
_buildTableCell(detail.quantite.toString()),
|
_buildTableCell(detail.quantite.toString()),
|
||||||
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
|
_buildTableCell(
|
||||||
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'),
|
'${detail.prixUnitaire.toStringAsFixed(2)} DA'),
|
||||||
|
_buildTableCell(
|
||||||
|
'${detail.sousTotal.toStringAsFixed(2)} DA'),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@ -434,7 +285,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
borderRadius: pw.BorderRadius.circular(8),
|
borderRadius: pw.BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: pw.Text(
|
child: pw.Text(
|
||||||
'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} MGA',
|
'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} DA',
|
||||||
style: pw.TextStyle(
|
style: pw.TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: pw.FontWeight.bold,
|
fontWeight: pw.FontWeight.bold,
|
||||||
@ -480,149 +331,19 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sauvegarder le PDF
|
||||||
final output = await getTemporaryDirectory();
|
final output = await getTemporaryDirectory();
|
||||||
final file = File('${output.path}/facture_${commande.id}.pdf');
|
final file = File('${output.path}/facture_${commande.id}.pdf');
|
||||||
await file.writeAsBytes(await pdf.save());
|
await file.writeAsBytes(await pdf.save());
|
||||||
await OpenFile.open(file.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async {
|
// Ouvrir le PDF
|
||||||
final details = await _database.getDetailsCommande(commande.id!);
|
|
||||||
final client = await _database.getClientById(commande.clientId);
|
|
||||||
final commandeur = commande.commandeurId != null
|
|
||||||
? await _database.getUserById(commande.commandeurId!)
|
|
||||||
: null;
|
|
||||||
final validateur = commande.validateurId != null
|
|
||||||
? await _database.getUserById(commande.validateurId!)
|
|
||||||
: null;
|
|
||||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
|
||||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final pdf = pw.Document();
|
|
||||||
final imageBytes = await loadImage();
|
|
||||||
final image = pw.MemoryImage(imageBytes);
|
|
||||||
|
|
||||||
pdf.addPage(
|
|
||||||
pw.Page(
|
|
||||||
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
|
|
||||||
margin: const pw.EdgeInsets.all(4),
|
|
||||||
build: (pw.Context context) {
|
|
||||||
return pw.Column(
|
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// En-tête
|
|
||||||
pw.Center(
|
|
||||||
child: pw.Container(
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
child: pw.Image(image),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pw.SizedBox(height: 4),
|
|
||||||
pw.Text('TICKET DE CAISSE',
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pw.Text('N°: ${commande.id}',
|
|
||||||
style: const pw.TextStyle(fontSize: 8)),
|
|
||||||
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
|
|
||||||
style: const pw.TextStyle(fontSize: 8)),
|
|
||||||
|
|
||||||
if (pointDeVente != null)
|
|
||||||
pw.Text('Point de vente: ${pointDeVente['designation']}',
|
|
||||||
style: const pw.TextStyle(fontSize: 8)),
|
|
||||||
|
|
||||||
pw.Divider(thickness: 0.5),
|
|
||||||
|
|
||||||
// Client
|
|
||||||
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
|
|
||||||
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
|
|
||||||
|
|
||||||
// Personnel
|
|
||||||
if (commandeur != null)
|
|
||||||
pw.Text('Commandeur: ${commandeur.name} ',
|
|
||||||
style: const pw.TextStyle(fontSize: 7)),
|
|
||||||
if (validateur != null)
|
|
||||||
pw.Text('Validateur: ${validateur.name}',
|
|
||||||
style: const pw.TextStyle(fontSize: 7)),
|
|
||||||
|
|
||||||
pw.Divider(thickness: 0.5),
|
|
||||||
|
|
||||||
// Détails
|
|
||||||
pw.Table(
|
|
||||||
columnWidths: {
|
|
||||||
0: const pw.FlexColumnWidth(3),
|
|
||||||
1: const pw.FlexColumnWidth(1),
|
|
||||||
2: const pw.FlexColumnWidth(2),
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
pw.TableRow(
|
|
||||||
children: [
|
|
||||||
pw.Text('Produit', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
|
||||||
pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
|
||||||
pw.Text('Total', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
...details.map((detail) => pw.TableRow(
|
|
||||||
children: [
|
|
||||||
pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)),
|
|
||||||
pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)),
|
|
||||||
pw.Text('${detail.sousTotal.toStringAsFixed(2)} MGA', style: const pw.TextStyle(fontSize: 7)),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
pw.Divider(thickness: 0.5),
|
|
||||||
|
|
||||||
// Total
|
|
||||||
pw.Row(
|
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
|
||||||
pw.Text('${commande.montantTotal.toStringAsFixed(2)} MGA',
|
|
||||||
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Paiement
|
|
||||||
pw.SizedBox(height: 8),
|
|
||||||
pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)),
|
|
||||||
pw.Text(
|
|
||||||
payment.type == PaymentType.cash
|
|
||||||
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(2)} MGA)'
|
|
||||||
: 'CARTE BANCAIRE',
|
|
||||||
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal)
|
|
||||||
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(2)} MGA',
|
|
||||||
style: const pw.TextStyle(fontSize: 8)),
|
|
||||||
|
|
||||||
pw.SizedBox(height: 12),
|
|
||||||
pw.Text('Merci pour votre achat !',
|
|
||||||
style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)),
|
|
||||||
pw.Text('www.guycom.mg',
|
|
||||||
style: const pw.TextStyle(fontSize: 7)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final output = await getTemporaryDirectory();
|
|
||||||
final file = File('${output.path}/ticket_${commande.id}.pdf');
|
|
||||||
await file.writeAsBytes(await pdf.save());
|
|
||||||
await OpenFile.open(file.path);
|
await OpenFile.open(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
|
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
|
||||||
return pw.Padding(
|
return pw.Padding(
|
||||||
padding: const pw.EdgeInsets.all(4.0),
|
padding: const pw.EdgeInsets.all(8.0),
|
||||||
child: pw.Text(text, style: style ?? pw.TextStyle(fontSize: 8)),
|
child: pw.Text(text, style: style ?? pw.TextStyle(fontSize: 10)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,7 +384,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Gestion des Commandes'),
|
appBar: const CustomAppBar(title: 'Gestion des Commandes'),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -692,13 +413,14 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black.withOpacity(0.1),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),)
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/logo.png',
|
'assets/logo.png', // Remplacez par le chemin de votre logo
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -753,7 +475,8 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black.withOpacity(0.1),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),)
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
@ -943,7 +666,8 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black.withOpacity(0.1),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),)
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
padding:
|
padding:
|
||||||
@ -1187,7 +911,6 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
_CommandeActions(
|
_CommandeActions(
|
||||||
commande: commande,
|
commande: commande,
|
||||||
onStatutChanged: _updateStatut,
|
onStatutChanged: _updateStatut,
|
||||||
onPaymentSelected: _showPaymentOptions,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1235,7 +958,7 @@ class _CommandeDetails extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<List<DetailCommande>>(
|
return FutureBuilder<List<DetailCommande>>(
|
||||||
future: AppDatabase.instance.getDetailsCommande(commande.id!),
|
future: ProductDatabase.instance.getDetailsCommande(commande.id!),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@ -1285,13 +1008,16 @@ class _CommandeDetails extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
...details.map((detail) => TableRow(
|
...details.map((detail) => TableRow(
|
||||||
children: [
|
children: [
|
||||||
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
|
_buildTableCell(
|
||||||
_buildTableCell('${detail.quantite}'),
|
detail.produitNom ?? 'Produit inconnu'),
|
||||||
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
|
_buildTableCell('${detail.quantite}'),
|
||||||
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'),
|
_buildTableCell(
|
||||||
],
|
'${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
|
||||||
)),
|
_buildTableCell(
|
||||||
|
'${detail.sousTotal.toStringAsFixed(2)} MGA'),
|
||||||
|
],
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1359,12 +1085,10 @@ class _CommandeDetails extends StatelessWidget {
|
|||||||
class _CommandeActions extends StatelessWidget {
|
class _CommandeActions extends StatelessWidget {
|
||||||
final Commande commande;
|
final Commande commande;
|
||||||
final Function(int, StatutCommande) onStatutChanged;
|
final Function(int, StatutCommande) onStatutChanged;
|
||||||
final Function(Commande) onPaymentSelected;
|
|
||||||
|
|
||||||
const _CommandeActions({
|
const _CommandeActions({
|
||||||
required this.commande,
|
required this.commande,
|
||||||
required this.onStatutChanged,
|
required this.onStatutChanged,
|
||||||
required this.onPaymentSelected,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1407,7 +1131,12 @@ class _CommandeActions extends StatelessWidget {
|
|||||||
label: 'Confirmer',
|
label: 'Confirmer',
|
||||||
icon: Icons.check_circle,
|
icon: Icons.check_circle,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: () => onPaymentSelected(commande),
|
onPressed: () => _showConfirmDialog(
|
||||||
|
context,
|
||||||
|
'Confirmer la commande',
|
||||||
|
'Êtes-vous sûr de vouloir confirmer cette commande?',
|
||||||
|
() => onStatutChanged(commande.id!, StatutCommande.confirmee),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
label: 'Annuler',
|
label: 'Annuler',
|
||||||
@ -1424,8 +1153,75 @@ class _CommandeActions extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
|
buttons.addAll([
|
||||||
|
_buildActionButton(
|
||||||
|
label: 'Préparer',
|
||||||
|
icon: Icons.settings,
|
||||||
|
color: Colors.amber,
|
||||||
|
onPressed: () => _showConfirmDialog(
|
||||||
|
context,
|
||||||
|
'Marquer en préparation',
|
||||||
|
'La commande va être marquée comme étant en cours de préparation.',
|
||||||
|
() => onStatutChanged(commande.id!, StatutCommande.enPreparation),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildActionButton(
|
||||||
|
label: 'Annuler',
|
||||||
|
icon: Icons.cancel,
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: () => _showConfirmDialog(
|
||||||
|
context,
|
||||||
|
'Annuler la commande',
|
||||||
|
'Êtes-vous sûr de vouloir annuler cette commande?',
|
||||||
|
() => onStatutChanged(commande.id!, StatutCommande.annulee),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
case StatutCommande.enPreparation:
|
case StatutCommande.enPreparation:
|
||||||
|
buttons.addAll([
|
||||||
|
_buildActionButton(
|
||||||
|
label: 'Expédier',
|
||||||
|
icon: Icons.local_shipping,
|
||||||
|
color: Colors.purple,
|
||||||
|
onPressed: () => _showConfirmDialog(
|
||||||
|
context,
|
||||||
|
'Expédier la commande',
|
||||||
|
'La commande va être marquée comme expédiée.',
|
||||||
|
() => onStatutChanged(commande.id!, StatutCommande.expediee),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildActionButton(
|
||||||
|
label: 'Annuler',
|
||||||
|
icon: Icons.cancel,
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: () => _showConfirmDialog(
|
||||||
|
context,
|
||||||
|
'Annuler la commande',
|
||||||
|
'Êtes-vous sûr de vouloir annuler cette commande?',
|
||||||
|
() => onStatutChanged(commande.id!, StatutCommande.annulee),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
case StatutCommande.expediee:
|
case StatutCommande.expediee:
|
||||||
|
buttons.add(
|
||||||
|
_buildActionButton(
|
||||||
|
label: 'Marquer livrée',
|
||||||
|
icon: Icons.check_circle,
|
||||||
|
color: Colors.green,
|
||||||
|
onPressed: () => _showConfirmDialog(
|
||||||
|
context,
|
||||||
|
'Marquer comme livrée',
|
||||||
|
'La commande va être marquée comme livrée.',
|
||||||
|
() => onStatutChanged(commande.id!, StatutCommande.livree),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
case StatutCommande.livree:
|
case StatutCommande.livree:
|
||||||
buttons.add(
|
buttons.add(
|
||||||
Container(
|
Container(
|
||||||
@ -1442,7 +1238,7 @@ class _CommandeActions extends StatelessWidget {
|
|||||||
color: Colors.green.shade600, size: 16),
|
color: Colors.green.shade600, size: 16),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Commande confirmée',
|
'Commande livrée',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.green.shade700,
|
color: Colors.green.shade700,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -1562,251 +1358,3 @@ class _CommandeActions extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PaymentType { cash, card ,
|
|
||||||
|
|
||||||
mvola,
|
|
||||||
orange,
|
|
||||||
airtel
|
|
||||||
}
|
|
||||||
|
|
||||||
class PaymentMethod {
|
|
||||||
final PaymentType type;
|
|
||||||
final double amountGiven;
|
|
||||||
|
|
||||||
PaymentMethod({required this.type, this.amountGiven = 0});
|
|
||||||
}
|
|
||||||
|
|
||||||
class PaymentMethodDialog extends StatefulWidget {
|
|
||||||
final Commande commande;
|
|
||||||
|
|
||||||
const PaymentMethodDialog({super.key, required this.commande});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_PaymentMethodDialogState createState() => _PaymentMethodDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|
||||||
PaymentType _selectedPayment = PaymentType.cash;
|
|
||||||
final _amountController = TextEditingController();
|
|
||||||
void _validatePayment() {
|
|
||||||
if (_selectedPayment == PaymentType.cash) {
|
|
||||||
final amountGiven = double.tryParse(_amountController.text) ?? 0;
|
|
||||||
if (amountGiven < widget.commande.montantTotal) {
|
|
||||||
Get.snackbar(
|
|
||||||
'Erreur',
|
|
||||||
'Le montant donné est insuffisant',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.pop(context, PaymentMethod(
|
|
||||||
type: _selectedPayment,
|
|
||||||
amountGiven: _selectedPayment == PaymentType.cash
|
|
||||||
? double.parse(_amountController.text)
|
|
||||||
: widget.commande.montantTotal,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// Initialiser avec le montant total de la commande
|
|
||||||
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_amountController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
|
||||||
final change = amount - widget.commande.montantTotal;
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// Section Paiement mobile
|
|
||||||
const Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildMobileMoneyTile(
|
|
||||||
title: 'Mvola',
|
|
||||||
imagePath: 'assets/mvola.jpg',
|
|
||||||
value: PaymentType.mvola,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: _buildMobileMoneyTile(
|
|
||||||
title: 'Orange Money',
|
|
||||||
imagePath: 'assets/Orange_money.png',
|
|
||||||
value: PaymentType.orange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: _buildMobileMoneyTile(
|
|
||||||
title: 'Airtel Money',
|
|
||||||
imagePath: 'assets/airtel_money.png',
|
|
||||||
value: PaymentType.airtel,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Section Carte bancaire
|
|
||||||
const Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildPaymentMethodTile(
|
|
||||||
title: 'Carte bancaire',
|
|
||||||
icon: Icons.credit_card,
|
|
||||||
value: PaymentType.card,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Section Paiement en liquide
|
|
||||||
const Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildPaymentMethodTile(
|
|
||||||
title: 'Paiement en liquide',
|
|
||||||
icon: Icons.money,
|
|
||||||
value: PaymentType.cash,
|
|
||||||
),
|
|
||||||
if (_selectedPayment == PaymentType.cash) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextField(
|
|
||||||
controller: _amountController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Montant donné',
|
|
||||||
prefixText: 'MGA ',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
||||||
onChanged: (value) => setState(() {}),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: change >= 0 ? Colors.green : Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('Annuler', style: TextStyle(color: Colors.grey)),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue.shade800,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
onPressed: _validatePayment,
|
|
||||||
child: const Text('Confirmer'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMobileMoneyTile({
|
|
||||||
required String title,
|
|
||||||
required String imagePath,
|
|
||||||
required PaymentType value,
|
|
||||||
}) {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
side: BorderSide(
|
|
||||||
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
onTap: () => setState(() => _selectedPayment = value),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Image.asset(
|
|
||||||
imagePath,
|
|
||||||
height: 30,
|
|
||||||
width: 30,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
|
||||||
const Icon(Icons.mobile_friendly, size: 30),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPaymentMethodTile({
|
|
||||||
required String title,
|
|
||||||
required IconData icon,
|
|
||||||
required PaymentType value,
|
|
||||||
}) {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
side: BorderSide(
|
|
||||||
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
onTap: () => setState(() => _selectedPayment = value),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 24),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(title),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
|
|
||||||
import '../Models/produit.dart';
|
import '../Models/produit.dart';
|
||||||
//import '../Services/productDatabase.dart';
|
import '../Services/productDatabase.dart';
|
||||||
import 'gestionProduct.dart';
|
import 'gestionProduct.dart';
|
||||||
|
|
||||||
class EditProductPage extends StatelessWidget {
|
class EditProductPage extends StatelessWidget {
|
||||||
@ -32,7 +31,7 @@ class EditProductPage extends StatelessWidget {
|
|||||||
category: category,
|
category: category,
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppDatabase.instance.updateProduct(updatedProduct);
|
await ProductDatabase.instance.updateProduct(updatedProduct);
|
||||||
|
|
||||||
Get.to(GestionProduit());
|
Get.to(GestionProduit());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import '../Services/app_database.dart';
|
||||||
//import '../Services/app_database.dart';
|
|
||||||
|
|
||||||
class EditUserPage extends StatefulWidget {
|
class EditUserPage extends StatefulWidget {
|
||||||
final Users user;
|
final Users user;
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
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:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import '../Components/appDrawer.dart';
|
import '../Components/appDrawer.dart';
|
||||||
import '../Models/produit.dart';
|
import '../Models/produit.dart';
|
||||||
// import '../Services/productDatabase.dart';
|
import '../Services/productDatabase.dart';
|
||||||
import 'editProduct.dart';
|
import 'editProduct.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
class GestionProduit extends StatelessWidget {
|
class GestionProduit extends StatelessWidget {
|
||||||
final AppDatabase _productDatabase = AppDatabase.instance;
|
final ProductDatabase _productDatabase = ProductDatabase.instance;
|
||||||
|
|
||||||
GestionProduit({super.key});
|
GestionProduit({super.key});
|
||||||
|
|
||||||
@ -18,7 +17,7 @@ class GestionProduit extends StatelessWidget {
|
|||||||
final screenWidth = MediaQuery.of(context).size.width * 0.8;
|
final screenWidth = MediaQuery.of(context).size.width * 0.8;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Gestion des produits'),
|
appBar: const CustomAppBar(title: 'Gestion des produits'),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
body: FutureBuilder<List<Product>>(
|
body: FutureBuilder<List<Product>>(
|
||||||
future: _productDatabase.getProducts(),
|
future: _productDatabase.getProducts(),
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Models/Permission.dart';
|
import 'package:youmazgestion/Models/Permission.dart';
|
||||||
//import 'package:youmazgestion/Services/app_database.dart';
|
import 'package:youmazgestion/Services/app_database.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
|
|
||||||
class HandleUserRole extends StatefulWidget {
|
class HandleUserRole extends StatefulWidget {
|
||||||
const HandleUserRole({super.key});
|
const HandleUserRole({super.key});
|
||||||
@ -84,7 +83,7 @@ class _HandleUserRoleState extends State<HandleUserRole> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: "Gestion des rôles"),
|
appBar: const CustomAppBar(title: "Gestion des rôles"),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@ -2,10 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_core/src/get_main.dart';
|
import 'package:get/get_core/src/get_main.dart';
|
||||||
import 'package:youmazgestion/Models/produit.dart';
|
import 'package:youmazgestion/Models/produit.dart';
|
||||||
//import 'package:youmazgestion/Services/productDatabase.dart';
|
import 'package:youmazgestion/Services/productDatabase.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
|
|
||||||
class GestionStockPage extends StatefulWidget {
|
class GestionStockPage extends StatefulWidget {
|
||||||
const GestionStockPage({super.key});
|
const GestionStockPage({super.key});
|
||||||
@ -15,7 +14,7 @@ class GestionStockPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GestionStockPageState extends State<GestionStockPage> {
|
class _GestionStockPageState extends State<GestionStockPage> {
|
||||||
final AppDatabase _database = AppDatabase.instance;
|
final ProductDatabase _database = ProductDatabase.instance;
|
||||||
List<Product> _products = [];
|
List<Product> _products = [];
|
||||||
List<Product> _filteredProducts = [];
|
List<Product> _filteredProducts = [];
|
||||||
String? _selectedCategory;
|
String? _selectedCategory;
|
||||||
@ -80,7 +79,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Gestion des Stocks'),
|
appBar: const CustomAppBar(title: 'Gestion des Stocks'),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -1,403 +1,123 @@
|
|||||||
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 'package:youmazgestion/Models/client.dart';
|
import '../Components/appDrawer.dart';
|
||||||
import 'package:youmazgestion/Models/produit.dart';
|
import '../controller/HistoryController.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'listCommandeHistory.dart';
|
||||||
|
|
||||||
class HistoriquePage extends StatefulWidget {
|
|
||||||
const HistoriquePage({super.key});
|
|
||||||
|
|
||||||
|
class HistoryPage extends GetView<HistoryController> {
|
||||||
@override
|
@override
|
||||||
_HistoriquePageState createState() => _HistoriquePageState();
|
HistoryController controller = Get.put(HistoryController());
|
||||||
}
|
|
||||||
|
|
||||||
class _HistoriquePageState extends State<HistoriquePage> {
|
HistoryPage({super.key});
|
||||||
final AppDatabase _appDatabase = AppDatabase.instance;
|
|
||||||
List<Commande> _commandes = [];
|
|
||||||
bool _isLoading = true;
|
|
||||||
DateTimeRange? _dateRange;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadCommandes();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadCommandes() async {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final commandes = await _appDatabase.getCommandes();
|
|
||||||
setState(() {
|
|
||||||
_commandes = commandes;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
Get.snackbar(
|
|
||||||
'Erreur',
|
|
||||||
'Impossible de charger les commandes: ${e.toString()}',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _selectDateRange(BuildContext context) async {
|
|
||||||
final DateTimeRange? picked = await showDateRangePicker(
|
|
||||||
context: context,
|
|
||||||
firstDate: DateTime(2020),
|
|
||||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
|
||||||
initialDateRange: _dateRange ?? DateTimeRange(
|
|
||||||
start: DateTime.now().subtract(const Duration(days: 30)),
|
|
||||||
end: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (picked != null) {
|
|
||||||
setState(() {
|
|
||||||
_dateRange = picked;
|
|
||||||
});
|
|
||||||
_filterCommandes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _filterCommandes() {
|
|
||||||
final searchText = _searchController.text.toLowerCase();
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
_appDatabase.getCommandes().then((commandes) {
|
|
||||||
List<Commande> filtered = commandes;
|
|
||||||
|
|
||||||
// Filtre par date
|
|
||||||
if (_dateRange != null) {
|
|
||||||
filtered = filtered.where((commande) {
|
|
||||||
final date = commande.dateCommande;
|
|
||||||
return date.isAfter(_dateRange!.start) &&
|
|
||||||
date.isBefore(_dateRange!.end.add(const Duration(days: 1)));
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par recherche
|
|
||||||
if (searchText.isNotEmpty) {
|
|
||||||
filtered = filtered.where((commande) {
|
|
||||||
return commande.clientNom!.toLowerCase().contains(searchText) ||
|
|
||||||
commande.clientPrenom!.toLowerCase().contains(searchText) ||
|
|
||||||
commande.id.toString().contains(searchText);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_commandes = filtered;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showCommandeDetails(Commande commande) async {
|
|
||||||
final details = await _appDatabase.getDetailsCommande(commande.id!);
|
|
||||||
final client = await _appDatabase.getClientById(commande.clientId);
|
|
||||||
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
height: MediaQuery.of(context).size.height * 0.7,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Commande #${commande.id}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
Text(
|
|
||||||
'Client: ${client?.nom} ${client?.prenom}',
|
|
||||||
style: const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Date: ${commande.dateCommande}',
|
|
||||||
style: const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Statut: ${_getStatutText(commande.statut)}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: _getStatutColor(commande.statut),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const Text(
|
|
||||||
'Articles:',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: details.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final detail = details[index];
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.shopping_bag),
|
|
||||||
title: Text(detail.produitNom ?? 'Produit inconnu'),
|
|
||||||
subtitle: Text(
|
|
||||||
'${detail.quantite} x ${detail.prixUnitaire.toStringAsFixed(2)} DA',
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
'${detail.sousTotal.toStringAsFixed(2)} DA',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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 _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 {
|
|
||||||
await _appDatabase.updateStatutCommande(
|
|
||||||
commandeId, StatutCommande.livree);
|
|
||||||
Get.back(); // Ferme le bottom sheet
|
|
||||||
_loadCommandes();
|
|
||||||
Get.snackbar(
|
|
||||||
'Succès',
|
|
||||||
'Statut de la commande mis à jour',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Get.snackbar(
|
|
||||||
'Erreur',
|
|
||||||
'Impossible de mettre à jour le statut: ${e.toString()}',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: const CustomAppBar(title: 'Historique'),
|
||||||
title: const Text('Historique des Commandes'),
|
drawer: CustomDrawer(),
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
onPressed: _loadCommandes,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
child: ElevatedButton(
|
||||||
children: [
|
onPressed: () {
|
||||||
Expanded(
|
controller.refreshOrders();
|
||||||
child: TextField(
|
controller.onInit();
|
||||||
controller: _searchController,
|
},
|
||||||
decoration: InputDecoration(
|
style: ElevatedButton.styleFrom(
|
||||||
labelText: 'Rechercher',
|
backgroundColor: Colors.deepOrangeAccent,
|
||||||
prefixIcon: const Icon(Icons.search),
|
shape: RoundedRectangleBorder(
|
||||||
border: OutlineInputBorder(
|
borderRadius: BorderRadius.circular(20.0),
|
||||||
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),
|
child: const Text(
|
||||||
onPressed: () => _selectDateRange(context),
|
'Rafraîchir',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16.0,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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(
|
Expanded(
|
||||||
child: _isLoading
|
child: Obx(
|
||||||
? const Center(child: CircularProgressIndicator())
|
() {
|
||||||
: _commandes.isEmpty
|
final distinctDates = controller.workDays;
|
||||||
? const Center(
|
|
||||||
child: Text('Aucune commande trouvée'),
|
if (distinctDates.isEmpty) {
|
||||||
)
|
return const Center(
|
||||||
: ListView.builder(
|
child: Text(
|
||||||
itemCount: _commandes.length,
|
'Aucune journée de travail trouvée',
|
||||||
itemBuilder: (context, index) {
|
style: TextStyle(
|
||||||
final commande = _commandes[index];
|
fontSize: 18.0,
|
||||||
return Card(
|
fontWeight: FontWeight.bold,
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String formatDate(String date) {
|
||||||
|
try {
|
||||||
|
final parsedDate = DateFormat('dd-MM-yyyy').parse(date);
|
||||||
|
print('parsedDate1: $parsedDate');
|
||||||
|
final formattedDate = DateFormat('yyyy-MM-dd').format(parsedDate);
|
||||||
|
print('formattedDate1: $formattedDate');
|
||||||
|
return formattedDate;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error parsing date: $date');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// transformer string en DateTime
|
||||||
|
void navigateToDetailPage(String selectedDate) {
|
||||||
|
print('selectedDate: $selectedDate');
|
||||||
|
DateTime parsedDate = DateFormat('yyyy-MM-dd').parse(selectedDate);
|
||||||
|
print('parsedDate: $parsedDate');
|
||||||
|
|
||||||
|
Get.to(() => HistoryDetailPage(selectedDate: parsedDate));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ class HistoryDetailPage extends StatelessWidget {
|
|||||||
init: controller,
|
init: controller,
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Historique de la journée'),
|
appBar: const CustomAppBar(title: 'Historique de la journée'),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import '../Components/app_bar.dart';
|
import '../Components/app_bar.dart';
|
||||||
//import '../Services/app_database.dart';
|
import '../Services/app_database.dart';
|
||||||
import 'editUser.dart';
|
import 'editUser.dart';
|
||||||
|
|
||||||
class ListUserPage extends StatefulWidget {
|
class ListUserPage extends StatefulWidget {
|
||||||
@ -36,7 +35,7 @@ class _ListUserPageState extends State<ListUserPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: 'Liste des utilisateurs'),
|
appBar: const CustomAppBar(title: 'Liste des utilisateurs'),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: userList.length,
|
itemCount: userList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Views/Dashboard.dart';
|
|
||||||
import 'package:youmazgestion/Views/mobilepage.dart';
|
|
||||||
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
||||||
import 'package:youmazgestion/accueil.dart';
|
import 'package:youmazgestion/accueil.dart';
|
||||||
|
import 'package:youmazgestion/Services/app_database.dart';
|
||||||
|
|
||||||
import '../Models/users.dart';
|
import '../Models/users.dart';
|
||||||
import '../controller/userController.dart';
|
import '../controller/userController.dart';
|
||||||
|
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
void checkUserCount() async {
|
void checkUserCount() async {
|
||||||
try {
|
try {
|
||||||
final userCount = await AppDatabase.instance.getUserCount();
|
final userCount = await AppDatabase.instance.getUserCount();
|
||||||
print('Nombre d\'utilisateurs trouvés: $userCount');
|
print('Nombre d\'utilisateurs trouvés: $userCount'); // Debug
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
|
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -55,12 +54,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
Future<void> saveUserData(Users user, String role, int userId) async {
|
Future<void> saveUserData(Users user, String role, int userId) async {
|
||||||
try {
|
try {
|
||||||
userController.setUserWithCredentials(user, role, userId);
|
userController.setUserWithCredentials(user, role, userId);
|
||||||
|
print(
|
||||||
if (user.pointDeVenteId != null) {
|
'Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId');
|
||||||
await userController.loadPointDeVenteDesignation();
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
print('Erreur lors de la sauvegarde: $error');
|
print('Erreur lors de la sauvegarde: $error');
|
||||||
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
||||||
@ -88,50 +83,27 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('Tentative de connexion pour: $username');
|
|
||||||
final dbInstance = AppDatabase.instance;
|
final dbInstance = AppDatabase.instance;
|
||||||
|
|
||||||
try {
|
// Vérifier les identifiants
|
||||||
final userCount = await dbInstance.getUserCount();
|
|
||||||
print('Base de données accessible, $userCount utilisateurs trouvés');
|
|
||||||
} catch (dbError) {
|
|
||||||
throw Exception('Impossible d\'accéder à la base de données: $dbError');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isValidUser = await dbInstance.verifyUser(username, password);
|
bool isValidUser = await dbInstance.verifyUser(username, password);
|
||||||
|
|
||||||
if (isValidUser) {
|
if (isValidUser) {
|
||||||
Users user = await dbInstance.getUser(username);
|
Users user = await dbInstance.getUser(username);
|
||||||
print('Utilisateur récupéré: ${user.username}');
|
|
||||||
|
|
||||||
Map<String, dynamic>? userCredentials =
|
Map<String, dynamic>? userCredentials =
|
||||||
await dbInstance.getUserCredentials(username, password);
|
await dbInstance.getUserCredentials(username, password);
|
||||||
|
|
||||||
if (userCredentials != null) {
|
if (userCredentials != null) {
|
||||||
print('Connexion réussie pour: ${user.username}');
|
|
||||||
print('Rôle: ${userCredentials['role']}');
|
|
||||||
print('ID: ${userCredentials['id']}');
|
|
||||||
|
|
||||||
await saveUserData(
|
await saveUserData(
|
||||||
user,
|
user,
|
||||||
userCredentials['role'] as String,
|
userCredentials['role'] as String,
|
||||||
userCredentials['id'] as int,
|
userCredentials['id'] as int,
|
||||||
);
|
);
|
||||||
|
|
||||||
// MODIFICATION PRINCIPALE ICI
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (userCredentials['role'] == 'commercial') {
|
Navigator.pushReplacement(
|
||||||
// Redirection vers MainLayout pour les commerciaux
|
context,
|
||||||
Navigator.pushReplacement(
|
MaterialPageRoute(builder: (context) => const AccueilPage()),
|
||||||
context,
|
);
|
||||||
MaterialPageRoute(builder: (context) => const MainLayout()),
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => DashboardPage()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Erreur lors de la récupération des credentials');
|
throw Exception('Erreur lors de la récupération des credentials');
|
||||||
@ -216,88 +188,124 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
],
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.lock_outline,
|
|
||||||
size: 100.0,
|
|
||||||
color: Color.fromARGB(255, 4, 54, 95),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: _usernameController,
|
|
||||||
enabled: !_isLoading,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Username',
|
|
||||||
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 16.0),
|
TextField(
|
||||||
TextField(
|
controller: _usernameController,
|
||||||
controller: _passwordController,
|
enabled: !_isLoading,
|
||||||
enabled: !_isLoading,
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
labelText: 'Nom d\'utilisateur',
|
||||||
labelText: 'Password',
|
labelStyle: TextStyle(
|
||||||
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent),
|
color: primaryColor.withOpacity(0.7),
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
prefixIcon: Icon(Icons.person, color: accentColor),
|
||||||
|
filled: true,
|
||||||
|
fillColor: accentColor.withOpacity(0.045),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
borderSide: BorderSide(color: accentColor, width: 2),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
borderSide: BorderSide(color: accentColor, width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
obscureText: true,
|
const SizedBox(height: 18.0),
|
||||||
onSubmitted: (_) => _login(),
|
TextField(
|
||||||
),
|
controller: _passwordController,
|
||||||
const SizedBox(height: 16.0),
|
enabled: !_isLoading,
|
||||||
Visibility(
|
obscureText: true,
|
||||||
visible: _isErrorVisible,
|
decoration: InputDecoration(
|
||||||
child: Text(
|
labelText: 'Mot de passe',
|
||||||
_errorMessage,
|
labelStyle: TextStyle(
|
||||||
style: const TextStyle(
|
color: primaryColor.withOpacity(0.7),
|
||||||
color: Colors.red,
|
),
|
||||||
fontSize: 14,
|
prefixIcon: Icon(Icons.lock, color: accentColor),
|
||||||
|
filled: true,
|
||||||
|
fillColor: accentColor.withOpacity(0.045),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
borderSide: BorderSide(color: accentColor, width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
onSubmitted: (_) => _login(),
|
||||||
),
|
),
|
||||||
),
|
if (_isErrorVisible) ...[
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 12.0),
|
||||||
ElevatedButton(
|
Text(
|
||||||
onPressed: _isLoading ? null : _login,
|
_errorMessage,
|
||||||
style: ElevatedButton.styleFrom(
|
style: const TextStyle(
|
||||||
backgroundColor: const Color(0xFF0015B7),
|
color: Colors.redAccent,
|
||||||
elevation: 5.0,
|
fontSize: 15,
|
||||||
shape: RoundedRectangleBorder(
|
fontWeight: FontWeight.w600,
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
minimumSize: const Size(double.infinity, 48),
|
],
|
||||||
),
|
const SizedBox(height: 26.0),
|
||||||
child: _isLoading
|
ElevatedButton(
|
||||||
? const SizedBox(
|
onPressed: _isLoading ? null : _login,
|
||||||
height: 20,
|
style: ElevatedButton.styleFrom(
|
||||||
width: 20,
|
backgroundColor: accentColor,
|
||||||
child: CircularProgressIndicator(
|
disabledBackgroundColor: accentColor.withOpacity(0.3),
|
||||||
color: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
strokeWidth: 2,
|
elevation: 7.0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
),
|
||||||
|
minimumSize: const Size(double.infinity, 52),
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'Se connecter',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: .4,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const Text(
|
// Option debug, à enlever en prod
|
||||||
'Se connecter',
|
if (_isErrorVisible) ...[
|
||||||
style: TextStyle(
|
TextButton(
|
||||||
color: Colors.white,
|
onPressed: () async {
|
||||||
fontSize: 16,
|
try {
|
||||||
),
|
final count =
|
||||||
),
|
await AppDatabase.instance.getUserCount();
|
||||||
),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
]
|
SnackBar(
|
||||||
)
|
content: Text('$count utilisateurs trouvés')),
|
||||||
)
|
);
|
||||||
],
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Erreur: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Debug: Vérifier BDD'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,851 +0,0 @@
|
|||||||
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
Load Diff
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import 'package:youmazgestion/accueil.dart';
|
import 'package:youmazgestion/accueil.dart';
|
||||||
|
|
||||||
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
||||||
|
|
||||||
class RegistrationPage extends StatefulWidget {
|
class RegistrationPage extends StatefulWidget {
|
||||||
const RegistrationPage({super.key});
|
const RegistrationPage({super.key});
|
||||||
@ -24,9 +23,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
Role? _selectedRole;
|
Role? _selectedRole;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoadingRoles = true;
|
bool _isLoadingRoles = true;
|
||||||
List<Map<String, dynamic>> _availablePointsDeVente = [];
|
|
||||||
int? _selectedPointDeVenteId;
|
|
||||||
bool _isLoadingPointsDeVente = true;
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -39,38 +36,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_initializeDatabase();
|
_initializeDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeDatabase() async {
|
Future<void> _initializeDatabase() async {
|
||||||
try {
|
try {
|
||||||
await AppDatabase.instance.initDatabase();
|
await AppDatabase.instance.initDatabase();
|
||||||
await _loadRoles();
|
await _loadRoles();
|
||||||
await _loadPointsDeVente(); // Ajouté ici
|
} catch (error) {
|
||||||
} catch (error) {
|
print('Erreur lors de l\'initialisation: $error');
|
||||||
print('Erreur lors de l\'initialisation: $error');
|
if (mounted) {
|
||||||
if (mounted) {
|
_showErrorDialog('Erreur d\'initialisation',
|
||||||
_showErrorDialog('Erreur d\'initialisation',
|
'Impossible d\'initialiser l\'application. Veuillez redémarrer.');
|
||||||
'Impossible d\'initialiser l\'application. Veuillez redémarrer.');
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Future<void> _loadPointsDeVente() async {
|
|
||||||
try {
|
|
||||||
final points = await AppDatabase.instance.getPointsDeVente();
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_availablePointsDeVente = points;
|
|
||||||
_isLoadingPointsDeVente = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
print('Erreur lors du chargement des points de vente: $error');
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_isLoadingPointsDeVente = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Future<void> _loadRoles() async {
|
Future<void> _loadRoles() async {
|
||||||
try {
|
try {
|
||||||
final roles = await AppDatabase.instance.getRoles();
|
final roles = await AppDatabase.instance.getRoles();
|
||||||
@ -120,16 +98,15 @@ Future<void> _loadPointsDeVente() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _validateFields() {
|
bool _validateFields() {
|
||||||
if (_nameController.text.trim().isEmpty ||
|
if (_nameController.text.trim().isEmpty ||
|
||||||
_lastNameController.text.trim().isEmpty ||
|
_lastNameController.text.trim().isEmpty ||
|
||||||
_emailController.text.trim().isEmpty ||
|
_emailController.text.trim().isEmpty ||
|
||||||
_usernameController.text.trim().isEmpty ||
|
_usernameController.text.trim().isEmpty ||
|
||||||
_passwordController.text.trim().isEmpty ||
|
_passwordController.text.trim().isEmpty ||
|
||||||
_selectedRole == null ||
|
_selectedRole == null) {
|
||||||
_selectedPointDeVenteId == null) { // Ajouté ici
|
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
|
||||||
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Validation basique de l'email
|
// Validation basique de l'email
|
||||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(_emailController.text.trim())) {
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(_emailController.text.trim())) {
|
||||||
@ -149,24 +126,23 @@ Future<void> _loadPointsDeVente() async {
|
|||||||
void _register() async {
|
void _register() async {
|
||||||
if (_isLoading) return;
|
if (_isLoading) return;
|
||||||
|
|
||||||
if (!_validateFields()) return;
|
if (!_validateFields()) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Créer l'objet utilisateur avec le nouveau modèle
|
// Créer l'objet utilisateur avec le nouveau modèle
|
||||||
final Users user = Users(
|
final Users user = Users(
|
||||||
name: _nameController.text.trim(),
|
name: _nameController.text.trim(),
|
||||||
lastName: _lastNameController.text.trim(),
|
lastName: _lastNameController.text.trim(),
|
||||||
email: _emailController.text.trim(),
|
email: _emailController.text.trim(),
|
||||||
password: _passwordController.text.trim(),
|
password: _passwordController.text.trim(),
|
||||||
username: _usernameController.text.trim(),
|
username: _usernameController.text.trim(),
|
||||||
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
|
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
|
||||||
roleName: _selectedRole!.designation, // Pour l'affichage
|
roleName: _selectedRole!.designation, // Pour l'affichage
|
||||||
pointDeVenteId: _selectedPointDeVenteId, // Ajouté ici
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// Sauvegarder l'utilisateur dans la base de données
|
// Sauvegarder l'utilisateur dans la base de données
|
||||||
final int userId = await AppDatabase.instance.createUser(user);
|
final int userId = await AppDatabase.instance.createUser(user);
|
||||||
@ -385,46 +361,6 @@ Future<void> _loadPointsDeVente() async {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Dans la méthode build, après le DropdownButton des rôles
|
|
||||||
const SizedBox(height: 16.0),
|
|
||||||
_isLoadingPointsDeVente
|
|
||||||
? const CircularProgressIndicator()
|
|
||||||
: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<int>(
|
|
||||||
value: _selectedPointDeVenteId,
|
|
||||||
hint: const Text('Sélectionner un point de vente'),
|
|
||||||
isExpanded: true,
|
|
||||||
onChanged: _isLoading
|
|
||||||
? null
|
|
||||||
: (int? newValue) {
|
|
||||||
setState(() {
|
|
||||||
_selectedPointDeVenteId = newValue;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
items: _availablePointsDeVente
|
|
||||||
.map<DropdownMenuItem<int>>((Map<String, dynamic> point) {
|
|
||||||
return DropdownMenuItem<int>(
|
|
||||||
value: point['id'] as int,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.store, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(point['designation'] as String),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16.0),
|
|
||||||
const SizedBox(height: 24.0),
|
const SizedBox(height: 24.0),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
||||||
import 'package:youmazgestion/Views/produitsCard.dart';
|
import 'package:youmazgestion/Views/produitsCard.dart';
|
||||||
import 'Components/appDrawer.dart';
|
import 'Components/appDrawer.dart';
|
||||||
@ -11,7 +10,7 @@ import 'Components/app_bar.dart';
|
|||||||
import 'Components/cartItem.dart';
|
import 'Components/cartItem.dart';
|
||||||
import 'Models/produit.dart';
|
import 'Models/produit.dart';
|
||||||
import 'Services/OrderDatabase.dart';
|
import 'Services/OrderDatabase.dart';
|
||||||
//import 'Services/productDatabase.dart';
|
import 'Services/productDatabase.dart';
|
||||||
import 'Views/ticketPage.dart';
|
import 'Views/ticketPage.dart';
|
||||||
import 'controller/userController.dart';
|
import 'controller/userController.dart';
|
||||||
import 'my_app.dart';
|
import 'my_app.dart';
|
||||||
@ -26,7 +25,7 @@ class AccueilPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _AccueilPageState extends State<AccueilPage> {
|
class _AccueilPageState extends State<AccueilPage> {
|
||||||
final UserController userController = Get.put(UserController());
|
final UserController userController = Get.put(UserController());
|
||||||
final AppDatabase productDatabase = AppDatabase.instance;
|
final ProductDatabase productDatabase = ProductDatabase();
|
||||||
late Future<Map<String, List<Product>>> productsFuture;
|
late Future<Map<String, List<Product>>> productsFuture;
|
||||||
final OrderDatabase orderDatabase = OrderDatabase.instance;
|
final OrderDatabase orderDatabase = OrderDatabase.instance;
|
||||||
final WorkDatabase workDatabase = WorkDatabase.instance;
|
final WorkDatabase workDatabase = WorkDatabase.instance;
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import 'package:youmazgestion/controller/userController.dart';
|
import 'package:youmazgestion/controller/userController.dart';
|
||||||
import '../Components/cartItem.dart';
|
import '../Components/cartItem.dart';
|
||||||
import '../Models/produit.dart';
|
import '../Models/produit.dart';
|
||||||
import '../Services/OrderDatabase.dart';
|
import '../Services/OrderDatabase.dart';
|
||||||
import '../Services/WorkDatabase.dart';
|
import '../Services/WorkDatabase.dart';
|
||||||
//import '../Services/productDatabase.dart';
|
import '../Services/productDatabase.dart';
|
||||||
import '../Views/ticketPage.dart';
|
import '../Views/ticketPage.dart';
|
||||||
import '../my_app.dart';
|
import '../my_app.dart';
|
||||||
|
|
||||||
class AccueilController extends GetxController {
|
class AccueilController extends GetxController {
|
||||||
final UserController userController = Get.find();
|
final UserController userController = Get.find();
|
||||||
final AppDatabase productDatabase = AppDatabase.instance;
|
final ProductDatabase productDatabase = ProductDatabase();
|
||||||
final Rx<Map<String, List<Product>>> productsFuture = Rx({}); // Observable
|
final Rx<Map<String, List<Product>>> productsFuture = Rx({}); // Observable
|
||||||
final OrderDatabase orderDatabase = OrderDatabase.instance;
|
final OrderDatabase orderDatabase = OrderDatabase.instance;
|
||||||
final WorkDatabase workDatabase = WorkDatabase.instance;
|
final WorkDatabase workDatabase = WorkDatabase.instance;
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/app_database.dart';
|
||||||
//import 'package:youmazgestion/Services/app_database.dart';
|
|
||||||
|
|
||||||
class UserController extends GetxController {
|
class UserController extends GetxController {
|
||||||
final _username = ''.obs;
|
final _username = ''.obs;
|
||||||
@ -12,8 +11,6 @@ class UserController extends GetxController {
|
|||||||
final _lastname = ''.obs;
|
final _lastname = ''.obs;
|
||||||
final _password = ''.obs;
|
final _password = ''.obs;
|
||||||
final _userId = 0.obs; // ✅ Ajout de l'ID utilisateur
|
final _userId = 0.obs; // ✅ Ajout de l'ID utilisateur
|
||||||
final _pointDeVenteId = 0.obs;
|
|
||||||
final _pointDeVenteDesignation = ''.obs;
|
|
||||||
|
|
||||||
String get username => _username.value;
|
String get username => _username.value;
|
||||||
String get email => _email.value;
|
String get email => _email.value;
|
||||||
@ -22,8 +19,6 @@ class UserController extends GetxController {
|
|||||||
String get lastname => _lastname.value;
|
String get lastname => _lastname.value;
|
||||||
String get password => _password.value;
|
String get password => _password.value;
|
||||||
int get userId => _userId.value;
|
int get userId => _userId.value;
|
||||||
int get pointDeVenteId => _pointDeVenteId.value;
|
|
||||||
String get pointDeVenteDesignation => _pointDeVenteDesignation.value;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -32,64 +27,48 @@ class UserController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
|
// ✅ CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
|
||||||
Future<void> loadUserData() async {
|
Future<void> loadUserData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
final storedUsername = prefs.getString('username') ?? '';
|
final storedUsername = prefs.getString('username') ?? '';
|
||||||
final storedRole = prefs.getString('role') ?? '';
|
final storedRole = prefs.getString('role') ?? '';
|
||||||
final storedUserId = prefs.getInt('user_id') ?? 0;
|
final storedUserId = prefs.getInt('user_id') ?? 0;
|
||||||
final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0;
|
|
||||||
final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? '';
|
|
||||||
|
|
||||||
if (storedUsername.isNotEmpty) {
|
if (storedUsername.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
Users user = await AppDatabase.instance.getUser(storedUsername);
|
// Récupérer les données complètes depuis la base de données
|
||||||
|
Users user = await AppDatabase.instance.getUser(storedUsername);
|
||||||
|
|
||||||
_username.value = user.username;
|
// Mettre à jour TOUTES les données
|
||||||
_email.value = user.email;
|
_username.value = user.username;
|
||||||
_name.value = user.name;
|
_email.value = user.email;
|
||||||
_lastname.value = user.lastName;
|
_name.value = user.name;
|
||||||
_password.value = user.password;
|
_lastname.value = user.lastName;
|
||||||
_role.value = storedRole;
|
_password.value = user.password;
|
||||||
_userId.value = storedUserId;
|
_role.value = storedRole; // Récupéré depuis SharedPreferences
|
||||||
_pointDeVenteId.value = storedPointDeVenteId;
|
_userId.value = storedUserId; // Récupéré depuis SharedPreferences
|
||||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
|
||||||
|
|
||||||
// Si la désignation n'est pas sauvegardée, on peut la récupérer
|
print("✅ Données chargées depuis la DB - Username: ${_username.value}");
|
||||||
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
|
print("✅ Name: ${_name.value}, Email: ${_email.value}");
|
||||||
await loadPointDeVenteDesignation();
|
print("✅ Role: ${_role.value}, UserID: ${_userId.value}");
|
||||||
|
} catch (dbError) {
|
||||||
|
print('❌ Erreur DB, chargement depuis SharedPreferences uniquement: $dbError');
|
||||||
|
// Fallback : charger depuis SharedPreferences uniquement
|
||||||
|
_username.value = storedUsername;
|
||||||
|
_email.value = prefs.getString('email') ?? '';
|
||||||
|
_role.value = storedRole;
|
||||||
|
_name.value = prefs.getString('name') ?? '';
|
||||||
|
_lastname.value = prefs.getString('lastname') ?? '';
|
||||||
|
_userId.value = storedUserId;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
} catch (dbError) {
|
print("❌ Aucun utilisateur stocké trouvé");
|
||||||
// Fallback
|
|
||||||
_username.value = storedUsername;
|
|
||||||
_email.value = prefs.getString('email') ?? '';
|
|
||||||
_role.value = storedRole;
|
|
||||||
_name.value = prefs.getString('name') ?? '';
|
|
||||||
_lastname.value = prefs.getString('lastname') ?? '';
|
|
||||||
_userId.value = storedUserId;
|
|
||||||
_pointDeVenteId.value = storedPointDeVenteId;
|
|
||||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Erreur lors du chargement des données utilisateur: $e');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
print('❌ Erreur lors du chargement des données utilisateur: $e');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Future<void> loadPointDeVenteDesignation() async {
|
|
||||||
if (_pointDeVenteId.value <= 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
|
|
||||||
if (pointDeVente != null) {
|
|
||||||
_pointDeVenteDesignation.value = pointDeVente['designation'] as String;
|
|
||||||
await saveUserData(); // Sauvegarder la désignation
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('❌ Erreur lors du chargement de la désignation du point de vente: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials
|
// ✅ NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials
|
||||||
void setUserWithCredentials(Users user, String role, int userId) {
|
void setUserWithCredentials(Users user, String role, int userId) {
|
||||||
@ -100,7 +79,6 @@ Future<void> loadPointDeVenteDesignation() async {
|
|||||||
_lastname.value = user.lastName;
|
_lastname.value = user.lastName;
|
||||||
_password.value = user.password;
|
_password.value = user.password;
|
||||||
_userId.value = userId; // ID depuis les credentials
|
_userId.value = userId; // ID depuis les credentials
|
||||||
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
|
|
||||||
|
|
||||||
print("✅ Utilisateur mis à jour avec credentials:");
|
print("✅ Utilisateur mis à jour avec credentials:");
|
||||||
print(" Username: ${_username.value}");
|
print(" Username: ${_username.value}");
|
||||||
@ -133,52 +111,49 @@ Future<void> loadPointDeVenteDesignation() async {
|
|||||||
|
|
||||||
// ✅ CORRECTION : Sauvegarder TOUTES les données importantes
|
// ✅ CORRECTION : Sauvegarder TOUTES les données importantes
|
||||||
Future<void> saveUserData() async {
|
Future<void> saveUserData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
await prefs.setString('username', _username.value);
|
await prefs.setString('username', _username.value);
|
||||||
await prefs.setString('email', _email.value);
|
await prefs.setString('email', _email.value);
|
||||||
await prefs.setString('role', _role.value);
|
await prefs.setString('role', _role.value);
|
||||||
await prefs.setString('name', _name.value);
|
await prefs.setString('name', _name.value);
|
||||||
await prefs.setString('lastname', _lastname.value);
|
await prefs.setString('lastname', _lastname.value);
|
||||||
await prefs.setInt('user_id', _userId.value);
|
await prefs.setInt('user_id', _userId.value); // ✅ Sauvegarder l'ID
|
||||||
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
|
|
||||||
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
|
|
||||||
|
|
||||||
print("✅ Données sauvegardées avec succès dans SharedPreferences");
|
print("✅ Données sauvegardées avec succès dans SharedPreferences");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e');
|
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ CORRECTION : Vider TOUTES les données (SharedPreferences + Observables)
|
// ✅ CORRECTION : Vider TOUTES les données (SharedPreferences + Observables)
|
||||||
Future<void> clearUserData() async {
|
Future<void> clearUserData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
await prefs.remove('username');
|
// Vider SharedPreferences
|
||||||
await prefs.remove('email');
|
await prefs.remove('username');
|
||||||
await prefs.remove('role');
|
await prefs.remove('email');
|
||||||
await prefs.remove('name');
|
await prefs.remove('role');
|
||||||
await prefs.remove('lastname');
|
await prefs.remove('name');
|
||||||
await prefs.remove('user_id');
|
await prefs.remove('lastname');
|
||||||
await prefs.remove('point_de_vente_id');
|
await prefs.remove('user_id'); // ✅ Supprimer l'ID aussi
|
||||||
await prefs.remove('point_de_vente_designation');
|
|
||||||
|
|
||||||
_username.value = '';
|
// Vider les variables observables
|
||||||
_email.value = '';
|
_username.value = '';
|
||||||
_role.value = '';
|
_email.value = '';
|
||||||
_name.value = '';
|
_role.value = '';
|
||||||
_lastname.value = '';
|
_name.value = '';
|
||||||
_password.value = '';
|
_lastname.value = '';
|
||||||
_userId.value = 0;
|
_password.value = '';
|
||||||
_pointDeVenteId.value = 0;
|
_userId.value = 0; // ✅ Réinitialiser l'ID
|
||||||
_pointDeVenteDesignation.value = '';
|
|
||||||
|
|
||||||
} catch (e) {
|
print("✅ Toutes les données utilisateur ont été effacées");
|
||||||
print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
|
} catch (e) {
|
||||||
|
print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté
|
// ✅ MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté
|
||||||
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
|
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
|
||||||
@ -216,16 +191,14 @@ Future<void> clearUserData() async {
|
|||||||
|
|
||||||
// ✅ MÉTHODE DEBUG : Afficher l'état actuel
|
// ✅ MÉTHODE DEBUG : Afficher l'état actuel
|
||||||
void debugPrintUserState() {
|
void debugPrintUserState() {
|
||||||
print("=== ÉTAT UTILISATEUR ===");
|
print("=== ÉTAT UTILISATEUR ===");
|
||||||
print("Username: ${_username.value}");
|
print("Username: ${_username.value}");
|
||||||
print("Name: ${_name.value}");
|
print("Name: ${_name.value}");
|
||||||
print("Lastname: ${_lastname.value}");
|
print("Lastname: ${_lastname.value}");
|
||||||
print("Email: ${_email.value}");
|
print("Email: ${_email.value}");
|
||||||
print("Role: ${_role.value}");
|
print("Role: ${_role.value}");
|
||||||
print("UserID: ${_userId.value}");
|
print("UserID: ${_userId.value}");
|
||||||
print("PointDeVenteID: ${_pointDeVenteId.value}");
|
print("IsLoggedIn: $isLoggedIn");
|
||||||
print("PointDeVente: ${_pointDeVenteDesignation.value}");
|
print("========================");
|
||||||
print("IsLoggedIn: $isLoggedIn");
|
}
|
||||||
print("========================");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
//import 'package:youmazgestion/Services/app_database.dart';
|
import 'package:youmazgestion/Services/app_database.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
||||||
import 'package:youmazgestion/controller/userController.dart';
|
import 'package:youmazgestion/controller/userController.dart';
|
||||||
//import 'Services/productDatabase.dart';
|
import 'Services/productDatabase.dart';
|
||||||
import 'my_app.dart';
|
import 'my_app.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@ -12,14 +11,14 @@ void main() async {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialiser les bases de données une seule fois
|
// Initialiser les bases de données une seule fois
|
||||||
// await AppDatabase.instance.deleteDatabaseFile();
|
// await AppDatabase.instance.deleteDatabaseFile();
|
||||||
// await ProductDatabase.instance.deleteDatabaseFile();
|
// await ProductDatabase.instance.deleteDatabaseFile();
|
||||||
|
|
||||||
// await ProductDatabase.instance.initDatabase();
|
await ProductDatabase.instance.initDatabase();
|
||||||
await AppDatabase.instance.initDatabase();
|
await AppDatabase.instance.initDatabase();
|
||||||
|
|
||||||
// Afficher les informations de la base (pour debug)
|
// Afficher les informations de la base (pour debug)
|
||||||
// await AppDatabase.instance.printDatabaseInfo();
|
await AppDatabase.instance.printDatabaseInfo();
|
||||||
Get.put(
|
Get.put(
|
||||||
UserController()); // Ajoute ce code AVANT tout accès au UserController
|
UserController()); // Ajoute ce code AVANT tout accès au UserController
|
||||||
setupLogger();
|
setupLogger();
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import Foundation
|
|||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import mobile_scanner
|
|
||||||
import open_file_mac
|
import open_file_mac
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@ -17,7 +16,6 @@ import url_launcher_macos
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
|
||||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
24
pubspec.lock
24
pubspec.lock
@ -241,14 +241,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+4"
|
version: "0.9.3+4"
|
||||||
fl_chart:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: fl_chart
|
|
||||||
sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.65.0"
|
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -616,14 +608,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
version: "1.0.6"
|
||||||
mobile_scanner:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: mobile_scanner
|
|
||||||
sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "5.2.3"
|
|
||||||
msix:
|
msix:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1057,18 +1041,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_charts
|
name: syncfusion_flutter_charts
|
||||||
sha256: "0222ac9d8cb6c671f014effe9bd5c0aef35eadb16471355345ba87cc0ac007b3"
|
sha256: bdb7cc5814ceb187793cea587f4a5946afcffd96726b219cee79df8460f44b7b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "20.4.54"
|
version: "21.2.4"
|
||||||
syncfusion_flutter_core:
|
syncfusion_flutter_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_core
|
name: syncfusion_flutter_core
|
||||||
sha256: "3979f0b1c5a97422cadae52d476c21fa3e0fb671ef51de6cae1d646d8b99fe1f"
|
sha256: "8db8f55c77f56968681447d3837c10f27a9e861e238a898fda116c7531def979"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "20.4.54"
|
version: "21.2.10"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -50,7 +50,7 @@ dependencies:
|
|||||||
logging: ^1.2.0
|
logging: ^1.2.0
|
||||||
msix: ^3.7.0
|
msix: ^3.7.0
|
||||||
flutter_charts: ^0.5.1
|
flutter_charts: ^0.5.1
|
||||||
syncfusion_flutter_charts: ^20.4.48
|
syncfusion_flutter_charts: ^21.2.4
|
||||||
shelf: ^1.4.1
|
shelf: ^1.4.1
|
||||||
shelf_router: ^1.1.4
|
shelf_router: ^1.1.4
|
||||||
pdf: ^3.8.4
|
pdf: ^3.8.4
|
||||||
@ -62,8 +62,7 @@ dependencies:
|
|||||||
path_provider: ^2.0.15
|
path_provider: ^2.0.15
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
excel: ^2.0.1
|
excel: ^2.0.1
|
||||||
mobile_scanner: ^5.0.0 # ou la version la plus récente
|
|
||||||
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -102,9 +101,6 @@ flutter:
|
|||||||
- assets/database/usersdb.db
|
- assets/database/usersdb.db
|
||||||
- assets/database/work.db
|
- assets/database/work.db
|
||||||
- assets/database/roles.db
|
- assets/database/roles.db
|
||||||
- assets/airtel_money.png
|
|
||||||
- assets/mvola.jpg
|
|
||||||
- assets/Orange_money.png
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user