Compare commits

...

42 Commits

Author SHA1 Message Date
andrymodeste 13554ee49c push 09112025 4 weeks ago
andrymodeste c757bdb701 push finale 4 months ago
andrymodeste 4b4d9637fd 01082025_02 4 months ago
andrymodeste 2eeaeaa4c4 01082025_01 4 months ago
andrymodeste f509af1716 31072025 4 months ago
andrymodeste ab97716dd2 push 29072025_01 4 months ago
andrymodeste c5ef3ca0cb commit 28072025_03 4 months ago
andrymodeste f41fc922a6 commit de 28072025_02 4 months ago
andrymodeste d043f6f20b commit de 18072025 4 months ago
Stephane 13296529c3 client 4 months ago
Stephane 99d570bd3a requete pour retourner point de vente 4 months ago
andrymodeste 5ad019d35e commit de 25-07-2025 4 months ago
andrymodeste a52749c415 commit 24/07/2025-2 5 months ago
Stephane 3e6a81c2c3 demande de sortie 5 months ago
Stephane 2764111fa4 filtre sur la page demande de sortie 5 months ago
Stephane b415b9f501 qr code scanner 5 months ago
andrymodeste ab398bddc6 commit 24/07/2025 5 months ago
Stephane 01e9cabeba scan qr 5 months ago
Stephane 1f2a255719 add client also 5 months ago
Stephane 3fcc50195c email non obligatoire 5 months ago
Stephane 55822cd52e non obligatoire sur email 5 months ago
Stephane 6b83a1ab5e database 5 months ago
ASUS 14ce881a3c cacher boutton facture 5 months ago
ASUS 6bc8373cad fixed 5 months ago
b.razafimandimbihery 551831df74 change scan 5 months ago
b.razafimandimbihery cb59287bfd changement au niveau de scan 5 months ago
b.razafimandimbihery 55e896775d changement au nivaue de scan 5 months ago
Stephane 46826006c2 nif stat 6 months ago
Stephane e02c4c8bef change the facture to show only on superadmin 6 months ago
b.razafimandimbihery 332ed228ae fix bug commande 6 months ago
b.razafimandimbihery 2af3b01d92 last update update 6 months ago
Stephane be8c169ad1 qr code reference 6 months ago
ASUS 48ae916f02 date format 6 months ago
b.razafimandimbihery c0bbb0da2b lastlast update 6 months ago
b.razafimandimbihery 595b38e9fb adding barcode scanner 6 months ago
b.razafimandimbihery 525b09c81f scan code bar 6 months ago
b.razafimandimbihery b5a11aa3c9 migration mysql 6 months ago
b.razafimandimbihery 831cce13da last last last update 6 months ago
b.razafimandimbihery c8fedd08e5 last update 6 months ago
b.razafimandimbihery 9eafda610f commit fonctionnalite impec 6 months ago
b.razafimandimbihery 2bef06a2fe commit commit 6 months ago
b.razafimandimbihery 57ea91b3d7 maj dernier farany farany 6 months ago
  1. 3
      android/app/build.gradle.kts
  2. 5
      android/app/src/main/AndroidManifest.xml
  3. BIN
      assets/NotoEmoji-Regular.ttf
  4. BIN
      assets/Orange_money.png
  5. BIN
      assets/airtel_money.png
  6. BIN
      assets/fa-solid-900.ttf
  7. BIN
      assets/fonts/Roboto-Italic.ttf
  8. BIN
      assets/mvola.jpg
  9. 105
      ios/Runner.xcodeproj/project.pbxproj
  10. 3
      ios/Runner/Info.plist
  11. 1
      ios/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=b2e12b7ce127ba825ffc2656889f5368-json
  12. 430
      lib/Components/AddClient.dart
  13. 471
      lib/Components/AddClientForm.dart
  14. 176
      lib/Components/DiscountDialog.dart
  15. 349
      lib/Components/GiftaselectedButton.dart
  16. 271
      lib/Components/QrScan.dart
  17. 996
      lib/Components/appDrawer.dart
  18. 112
      lib/Components/app_bar.dart
  19. 11
      lib/Components/colors.dart
  20. 388
      lib/Components/commandManagementComponents/CommandDetails.dart
  21. 220
      lib/Components/commandManagementComponents/CommandeActions.dart
  22. 190
      lib/Components/commandManagementComponents/DiscountDialog.dart
  23. 136
      lib/Components/commandManagementComponents/GiftSelectionDialog.dart
  24. 234
      lib/Components/commandManagementComponents/PaswordRequired.dart
  25. 8
      lib/Components/commandManagementComponents/PaymentMethod.dart
  26. 266
      lib/Components/commandManagementComponents/PaymentMethodDialog.dart
  27. 7
      lib/Components/commandManagementComponents/PaymentType.dart
  28. 412
      lib/Components/newCommandComponents/CadeauDialog.dart
  29. 332
      lib/Components/newCommandComponents/RemiseDialog.dart
  30. 7
      lib/Components/paymentType.dart
  31. 831
      lib/Components/windows_qr_scanner.dart
  32. 366
      lib/Models/Client.dart
  33. 64
      lib/Models/Remise.dart
  34. 36
      lib/Models/pointage_model.dart
  35. 170
      lib/Models/produit.dart
  36. 33
      lib/Models/users.dart
  37. 0
      lib/Services/GestionStockDatabase.dart
  38. 258
      lib/Services/PermissionCacheService.dart
  39. 359
      lib/Services/Script.sql
  40. 680
      lib/Services/app_database.dart
  41. 559
      lib/Services/productDatabase.dart
  42. 1090
      lib/Services/qrService.dart
  43. 3565
      lib/Services/stock_managementDatabase.dart
  44. 2524
      lib/Views/Dashboard.dart
  45. 847
      lib/Views/DemandeTransfert.dart
  46. 5662
      lib/Views/HandleProduct.dart
  47. 7
      lib/Views/RoleListPage.dart
  48. 610
      lib/Views/RolePermissionPage.dart
  49. 452
      lib/Views/approbation_sorties_page.dart
  50. 5
      lib/Views/bilanMois.dart
  51. 4001
      lib/Views/commandManagement.dart
  52. 979
      lib/Views/demande_sortie_personnelle_page.dart
  53. 5
      lib/Views/editProduct.dart
  54. 418
      lib/Views/editUser.dart
  55. 7
      lib/Views/gestionProduct.dart
  56. 462
      lib/Views/gestionRole.dart
  57. 65
      lib/Views/gestionStock.dart
  58. 1121
      lib/Views/gestion_point_de_vente.dart
  59. 1042
      lib/Views/historique.dart
  60. 356
      lib/Views/historique_sorties_personnelles_page.dart
  61. 7
      lib/Views/listCommandeHistory.dart
  62. 700
      lib/Views/listUser.dart
  63. 636
      lib/Views/loginPage.dart
  64. 44
      lib/Views/mobilepage.dart
  65. 4928
      lib/Views/newCommand.dart
  66. 63
      lib/Views/produitsCard.dart
  67. 140
      lib/Views/registrationPage.dart
  68. 140
      lib/Views/ticketPage.dart
  69. 10
      lib/accueil.dart
  70. 101
      lib/config/DatabaseConfig.dart
  71. 5
      lib/controller/AccueilController.dart
  72. 31
      lib/controller/HistoryController.dart
  73. 203
      lib/controller/userController.dart
  74. 114
      lib/main.dart
  75. 58
      lib/my_app.dart
  76. 16
      linux/flutter/generated_plugin_registrant.cc
  77. 4
      linux/flutter/generated_plugins.cmake
  78. 10
      macos/Flutter/GeneratedPluginRegistrant.swift
  79. 296
      pubspec.lock
  80. 34
      pubspec.yaml
  81. 3
      test/widget_test.dart
  82. 15
      windows/flutter/generated_plugin_registrant.cc
  83. 5
      windows/flutter/generated_plugins.cmake
  84. BIN
      windows/runner/resources/app_icon.ico

3
android/app/build.gradle.kts

@ -8,7 +8,8 @@ plugins {
android {
namespace = "com.example.my_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion = "26.3.11579264"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11

5
android/app/src/main/AndroidManifest.xml

@ -1,6 +1,8 @@
<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
android:label="my_app"
android:label="GUYCOM"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@ -12,6 +14,7 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues

BIN
assets/NotoEmoji-Regular.ttf

Binary file not shown.

BIN
assets/Orange_money.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/airtel_money.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/fa-solid-900.ttf

Binary file not shown.

BIN
assets/fonts/Roboto-Italic.ttf

Binary file not shown.

BIN
assets/mvola.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

105
ios/Runner.xcodeproj/project.pbxproj

@ -9,36 +9,28 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
40B7AFF4F53F86CC7F95DB7E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3407068497C5195D266B3A97 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
ACFB5868BBA6B74A992E1A41 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FF94D6584D2E8A3F6BBFAD2 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1FF94D6584D2E8A3F6BBFAD2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2B3BBB8D343D07BA83A38E09 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
3407068497C5195D266B3A97 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
700DD0757496908A05DEFAD8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
4F258B4E2DF35D49004E4A7D /* PhoneNumberKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PhoneNumberKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4F258B5A2DF35FBA004E4A7D /* libphonenumber_plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumber_plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4F258B5B2DF35FBA004E4A7D /* PhoneNumberKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PhoneNumberKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4F258B5C2DF35FBA004E4A7D /* PhoneNumberKitPrivacy.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = PhoneNumberKitPrivacy.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7977BB33D07D0D6796B20A22 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
886ED87D91BD7C88314D0336 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9699BA5770E23C7B8F115644 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -46,7 +38,6 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F33AC98EB80D167B1D458E07 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -54,7 +45,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
ACFB5868BBA6B74A992E1A41 /* Pods_Runner.framework in Frameworks */,
40B7AFF4F53F86CC7F95DB7E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -79,7 +70,7 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
C2405E9D8DA19A1A863D1EDF /* Pods */,
A35EC737372E19786CB8B8AD /* Frameworks */,
B8985405432C97FEADFE4515 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -106,10 +97,14 @@
path = Runner;
sourceTree = "<group>";
};
A35EC737372E19786CB8B8AD /* Frameworks */ = {
B8985405432C97FEADFE4515 /* Frameworks */ = {
isa = PBXGroup;
children = (
1FF94D6584D2E8A3F6BBFAD2 /* Pods_Runner.framework */,
4F258B5A2DF35FBA004E4A7D /* libphonenumber_plugin.framework */,
4F258B5B2DF35FBA004E4A7D /* PhoneNumberKit.framework */,
4F258B5C2DF35FBA004E4A7D /* PhoneNumberKitPrivacy.bundle */,
4F258B4E2DF35D49004E4A7D /* PhoneNumberKit.framework */,
3407068497C5195D266B3A97 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -117,11 +112,10 @@
C2405E9D8DA19A1A863D1EDF /* Pods */ = {
isa = PBXGroup;
children = (
F33AC98EB80D167B1D458E07 /* Pods-Runner.debug.xcconfig */,
886ED87D91BD7C88314D0336 /* Pods-Runner.release.xcconfig */,
700DD0757496908A05DEFAD8 /* Pods-Runner.profile.xcconfig */,
2B3BBB8D343D07BA83A38E09 /* Pods-Runner.debug.xcconfig */,
7977BB33D07D0D6796B20A22 /* Pods-Runner.release.xcconfig */,
9699BA5770E23C7B8F115644 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@ -132,14 +126,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
40AEFA2DCA57AF98219D4A74 /* [CP] Check Pods Manifest.lock */,
65083E199DC1B675D18EE07F /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
731B87E0F0D7405841DD9A9D /* [CP] Embed Pods Frameworks */,
97B40C2864F2C81CADD5363C /* [CP] Embed Pods Frameworks */,
40D065C904D60CCA15650410 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -214,43 +208,43 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
40AEFA2DCA57AF98219D4A74 /* [CP] Check Pods Manifest.lock */ = {
40D065C904D60CCA15650410 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
731B87E0F0D7405841DD9A9D /* [CP] Embed Pods Frameworks */ = {
65083E199DC1B675D18EE07F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
@ -268,6 +262,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
97B40C2864F2C81CADD5363C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -366,7 +377,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.youmazgestion;
PRODUCT_BUNDLE_IDENTIFIER = flutter.c4m.mg;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -495,7 +506,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.youmazgestion;
PRODUCT_BUNDLE_IDENTIFIER = flutter.c4m.mg;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -518,7 +529,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.youmazgestion;
PRODUCT_BUNDLE_IDENTIFIER = flutter.c4m.mg;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

3
ios/Runner/Info.plist

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Cette application a besoin d'accéder à la caméra pour scanner les codes QR</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -47,5 +49,6 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

1
ios/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=b2e12b7ce127ba825ffc2656889f5368-json

@ -0,0 +1 @@
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/rabarisonmimistephanethannio/Documents/DEV/guycom_finale/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=7e9f9a517e1b730b3eb5b9aa5a52f2df_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}

430
lib/Components/AddClient.dart

@ -0,0 +1,430 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Models/client.dart';
import '../Services/stock_managementDatabase.dart';
class ClientFormController extends GetxController {
final _formKey = GlobalKey<FormState>();
// Controllers pour les champs
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _adresseController = TextEditingController();
// Variables observables pour la recherche
var suggestedClients = <Client>[].obs;
var isSearching = false.obs;
var selectedClient = Rxn<Client>();
@override
void onClose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
super.onClose();
}
// Méthode pour rechercher les clients existants
Future<void> searchClients(String query) async {
if (query.length < 2) {
suggestedClients.clear();
return;
}
isSearching.value = true;
try {
final clients = await AppDatabase.instance.suggestClients(query);
suggestedClients.value = clients;
} catch (e) {
print("Erreur recherche clients: $e");
suggestedClients.clear();
} finally {
isSearching.value = false;
}
}
// Méthode pour remplir automatiquement le formulaire
void fillFormWithClient(Client client) {
selectedClient.value = client;
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
_telephoneController.text = client.telephone;
_adresseController.text = client.adresse ?? '';
suggestedClients.clear();
}
// Méthode pour vider le formulaire
void clearForm() {
selectedClient.value = null;
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
suggestedClients.clear();
}
// Méthode pour valider et soumettre
Future<void> submitForm() async {
if (!_formKey.currentState!.validate()) return;
try {
Client clientToUse;
if (selectedClient.value != null) {
// Utiliser le client existant
clientToUse = selectedClient.value!;
} else {
// Créer un nouveau client
final newClient = Client(
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty
? null
: _adresseController.text.trim(),
dateCreation: DateTime.now(),
);
clientToUse = await AppDatabase.instance.createOrGetClient(newClient);
}
// Procéder avec la commande
Get.back();
_submitOrderWithClient(clientToUse);
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors de la création/récupération du client: $e',
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade800,
);
}
}
void _submitOrderWithClient(Client client) {
// Votre logique existante pour soumettre la commande
// avec le client fourni
}
}
// Widget pour le formulaire avec auto-completion
// ignore: unused_element
void _showClientFormDialog() {
final controller = Get.put(ClientFormController());
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'),
const Spacer(),
// Bouton pour vider le formulaire
IconButton(
onPressed: controller.clearForm,
icon: const Icon(Icons.clear),
tooltip: 'Vider le formulaire',
),
],
),
content: Container(
width: 600,
constraints: const BoxConstraints(maxHeight: 700),
child: SingleChildScrollView(
child: Form(
key: controller._formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section de recherche rapide
_buildSearchSection(controller),
const SizedBox(height: 16),
// Indicateur client sélectionné
Obx(() {
if (controller.selectedClient.value != null) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
border: Border.all(color: Colors.green.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.check_circle,
color: Colors.green.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'Client existant sélectionné: ${controller.selectedClient.value!.nomComplet}',
style: TextStyle(
color: Colors.green.shade800,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
return const SizedBox.shrink();
}),
const SizedBox(height: 12),
// Champs du formulaire
_buildTextFormField(
controller: controller._nomController,
label: 'Nom',
validator: (value) =>
value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._prenomController,
label: 'Prénom',
validator: (value) => value?.isEmpty ?? true
? 'Veuillez entrer un prénom'
: null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
// if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
if (value?.isEmpty ?? true) return null;
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value!)) {
return 'Email invalide';
}
return null;
},
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
// Recherche automatique par email
controller.searchClients(value);
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._telephoneController,
label: 'Téléphone',
keyboardType: TextInputType.phone,
validator: (value) => value?.isEmpty ?? true
? 'Veuillez entrer un téléphone'
: null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
// Recherche automatique par téléphone
controller.searchClients(value);
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: controller._adresseController,
label: 'Adresse',
maxLines: 2,
validator: (value) => value?.isEmpty ?? true
? 'Veuillez entrer une adresse'
: null,
onChanged: (value) {
if (controller.selectedClient.value != null) {
controller.selectedClient.value = null;
}
},
),
const SizedBox(height: 12),
_buildCommercialDropdown(),
// Liste des suggestions
Obx(() {
if (controller.isSearching.value) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
if (controller.suggestedClients.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
Text(
'Clients trouvés:',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
...controller.suggestedClients.map(
(client) =>
_buildClientSuggestionTile(client, controller),
),
],
);
}),
],
),
),
),
),
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: controller.submitForm,
child: const Text('Valider la commande'),
),
],
),
);
}
// Widget pour la section de recherche
Widget _buildSearchSection(ClientFormController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recherche rapide',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
TextFormField(
decoration: InputDecoration(
labelText: 'Rechercher un client existant',
hintText: 'Nom, prénom, email ou téléphone...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
onChanged: controller.searchClients,
),
],
);
}
// Widget pour afficher une suggestion de client
Widget _buildClientSuggestionTile(
Client client, ClientFormController controller) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: Icon(Icons.person, color: Colors.blue.shade700),
),
title: Text(
client.nomComplet,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('📧 ${client.email}'),
Text('📞 ${client.telephone}'),
if (client.adresse != null && client.adresse!.isNotEmpty)
Text('📍 ${client.adresse}'),
],
),
trailing: ElevatedButton(
onPressed: () => controller.fillFormWithClient(client),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('Utiliser'),
),
isThreeLine: true,
),
);
}
// Widget helper pour les champs de texte
Widget _buildTextFormField({
required TextEditingController controller,
required String label,
TextInputType? keyboardType,
String? Function(String?)? validator,
int maxLines = 1,
void Function(String)? onChanged,
}) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: keyboardType,
validator: validator,
maxLines: maxLines,
onChanged: onChanged,
);
}
// Votre méthode _buildCommercialDropdown existante
Widget _buildCommercialDropdown() {
// Votre implémentation existante
return Container(); // Remplacez par votre code existant
}

471
lib/Components/AddClientForm.dart

@ -0,0 +1,471 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Models/client.dart';
class ClientFormWidget extends StatefulWidget {
final Function(Client) onClientSelected;
final Client? initialClient;
const ClientFormWidget({
Key? key,
required this.onClientSelected,
this.initialClient,
}) : super(key: key);
@override
State<ClientFormWidget> createState() => _ClientFormWidgetState();
}
class _ClientFormWidgetState extends State<ClientFormWidget> {
final _formKey = GlobalKey<FormState>();
final AppDatabase _database = AppDatabase.instance;
// Contrôleurs de texte
final TextEditingController _nomController = TextEditingController();
final TextEditingController _prenomController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _telephoneController = TextEditingController();
final TextEditingController _adresseController = TextEditingController();
// Variables d'état
bool _isLoading = false;
Client? _selectedClient;
List<Client> _suggestions = [];
bool _showSuggestions = false;
String _searchQuery = '';
@override
void initState() {
super.initState();
if (widget.initialClient != null) {
_fillClientData(widget.initialClient!);
}
// Écouter les changements dans les champs pour déclencher la recherche
_emailController.addListener(_onEmailChanged);
_telephoneController.addListener(_onPhoneChanged);
_nomController.addListener(_onNameChanged);
_prenomController.addListener(_onNameChanged);
}
@override
void dispose() {
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
super.dispose();
}
void _fillClientData(Client client) {
setState(() {
_selectedClient = client;
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
_telephoneController.text = client.telephone;
_adresseController.text = client.adresse ?? '';
});
}
void _clearForm() {
setState(() {
_selectedClient = null;
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
_suggestions.clear();
_showSuggestions = false;
});
}
// Recherche par email
void _onEmailChanged() async {
final email = _emailController.text.trim();
if (email.length >= 3 && email.contains('@')) {
_searchExistingClient(email: email);
}
}
// Recherche par téléphone
void _onPhoneChanged() async {
final phone = _telephoneController.text.trim();
if (phone.length >= 4) {
_searchExistingClient(telephone: phone);
}
}
// Recherche par nom/prénom
void _onNameChanged() async {
final nom = _nomController.text.trim();
final prenom = _prenomController.text.trim();
if (nom.length >= 2 || prenom.length >= 2) {
final query = '$nom $prenom'.trim();
if (query.length >= 2) {
_getSuggestions(query);
}
}
}
// Rechercher un client existant
Future<void> _searchExistingClient({
String? email,
String? telephone,
String? nom,
String? prenom,
}) async {
if (_selectedClient != null) return; // Éviter de chercher si un client est déjà sélectionné
try {
setState(() => _isLoading = true);
final existingClient = await _database.findExistingClient(
email: email,
telephone: telephone,
nom: nom,
prenom: prenom,
);
if (existingClient != null && mounted) {
_showClientFoundDialog(existingClient);
}
} catch (e) {
print('Erreur lors de la recherche: $e');
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
// Obtenir les suggestions
Future<void> _getSuggestions(String query) async {
if (query.length < 2) {
setState(() {
_suggestions.clear();
_showSuggestions = false;
});
return;
}
try {
final suggestions = await _database.suggestClients(query);
if (mounted) {
setState(() {
_suggestions = suggestions;
_showSuggestions = suggestions.isNotEmpty;
_searchQuery = query;
});
}
} catch (e) {
print('Erreur lors de la récupération des suggestions: $e');
}
}
// Afficher le dialogue de client trouvé
void _showClientFoundDialog(Client client) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Client existant trouvé'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Un client avec ces informations existe déjà :'),
const SizedBox(height: 10),
Text('Nom: ${client.nom} ${client.prenom}', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('Email: ${client.email}'),
Text('Téléphone: ${client.telephone}'),
if (client.adresse != null) Text('Adresse: ${client.adresse}'),
const SizedBox(height: 10),
const Text('Voulez-vous utiliser ces informations ?'),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
// Continuer avec les nouvelles données
},
child: const Text('Non, créer nouveau'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_fillClientData(client);
},
child: const Text('Oui, utiliser'),
),
],
),
);
}
// Valider et soumettre le formulaire
void _submitForm() async {
if (!_formKey.currentState!.validate()) return;
try {
setState(() => _isLoading = true);
Client client;
if (_selectedClient != null) {
// Utiliser le client existant avec les données mises à jour
client = Client(
id: _selectedClient!.id,
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim().toLowerCase(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: _selectedClient!.dateCreation,
actif: _selectedClient!.actif,
);
} else {
// Créer un nouveau client
client = Client(
nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(),
email: _emailController.text.trim().toLowerCase(),
telephone: _telephoneController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
dateCreation: DateTime.now(),
);
// Utiliser createOrGetClient pour éviter les doublons
client = await _database.createOrGetClient(client);
}
widget.onClientSelected(client);
} catch (e) {
Get.snackbar(
'Erreur',
'Erreur lors de la sauvegarde du client: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// En-tête avec bouton de réinitialisation
Row(
children: [
const Text(
'Informations du client',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Spacer(),
if (_selectedClient != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Client existant',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _clearForm,
icon: const Icon(Icons.refresh),
tooltip: 'Nouveau client',
),
],
),
const SizedBox(height: 16),
// Champs du formulaire
Row(
children: [
Expanded(
child: TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom *',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est requis';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom *',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le prénom est requis';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Email avec indicateur de chargement
Stack(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email *',
border: const OutlineInputBorder(),
suffixIcon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'L\'email est requis';
}
if (!GetUtils.isEmail(value)) {
return 'Email invalide';
}
return null;
},
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone *',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le téléphone est requis';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(
labelText: 'Adresse',
border: OutlineInputBorder(),
),
maxLines: 2,
),
// Suggestions
if (_showSuggestions && _suggestions.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
const Icon(Icons.people, size: 16),
const SizedBox(width: 8),
const Text('Clients similaires trouvés:', style: TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
IconButton(
onPressed: () => setState(() => _showSuggestions = false),
icon: const Icon(Icons.close, size: 16),
),
],
),
),
...List.generate(_suggestions.length, (index) {
final suggestion = _suggestions[index];
return ListTile(
dense: true,
leading: const Icon(Icons.person, size: 20),
title: Text('${suggestion.nom} ${suggestion.prenom}'),
subtitle: Text('${suggestion.email}${suggestion.telephone}'),
trailing: ElevatedButton(
onPressed: () => _fillClientData(suggestion),
child: const Text('Utiliser'),
),
);
}),
],
),
),
],
const SizedBox(height: 24),
// Bouton de soumission
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 8),
Text('Traitement...'),
],
)
: Text(_selectedClient != null ? 'Utiliser ce client' : 'Créer le client'),
),
),
],
),
);
}
}

176
lib/Components/DiscountDialog.dart

@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/snackbar/snackbar.dart';
import 'package:youmazgestion/Models/Remise.dart';
class DiscountDialog extends StatefulWidget {
final Function(Remise) onDiscountApplied;
const DiscountDialog({super.key, required this.onDiscountApplied});
@override
_DiscountDialogState createState() => _DiscountDialogState();
}
class _DiscountDialogState extends State<DiscountDialog> {
RemiseType _selectedType = RemiseType.pourcentage;
final _valueController = TextEditingController();
final _descriptionController = TextEditingController();
@override
void dispose() {
_valueController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _applyDiscount() {
final value = double.tryParse(_valueController.text) ?? 0;
if (value <= 0) {
Get.snackbar(
'Erreur',
'Veuillez entrer une valeur valide',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
if (_selectedType == RemiseType.pourcentage && value > 100) {
Get.snackbar(
'Erreur',
'Le pourcentage ne peut pas dépasser 100%',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
final remise = Remise(
type: _selectedType,
valeur: value,
description: _descriptionController.text,
);
widget.onDiscountApplied(remise);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.local_offer, color: Colors.orange.shade600),
const SizedBox(width: 8),
const Text('Appliquer une remise'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Type de remise:', style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<RemiseType>(
contentPadding: EdgeInsets.zero,
title: const Text('Pourcentage'),
value: RemiseType.pourcentage,
groupValue: _selectedType,
onChanged: (value) => setState(() => _selectedType = value!),
),
),
Expanded(
child: RadioListTile<RemiseType>(
contentPadding: EdgeInsets.zero,
title: const Text('Montant fixe'),
value: RemiseType.fixe,
groupValue: _selectedType,
onChanged: (value) => setState(() => _selectedType = value!),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _valueController,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: _selectedType == RemiseType.pourcentage
? 'Pourcentage (%)'
: 'Montant (MGA)',
prefixIcon: Icon(
_selectedType == RemiseType.pourcentage
? Icons.percent
: Icons.attach_money,
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Motif de la remise (optionnel)',
prefixIcon: Icon(Icons.note),
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 16),
// Aperçu de la remise
if (_valueController.text.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Aperçu:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(
_selectedType == RemiseType.pourcentage
? 'Remise de ${_valueController.text}%'
: 'Remise de ${_valueController.text} MGA',
),
if (_descriptionController.text.isNotEmpty)
Text('Motif: ${_descriptionController.text}'),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _applyDiscount,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade600,
foregroundColor: Colors.white,
),
child: const Text('Appliquer'),
),
],
);
}
}

349
lib/Components/GiftaselectedButton.dart

@ -0,0 +1,349 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:youmazgestion/Models/Remise.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class GiftSelectionDialog extends StatefulWidget {
const GiftSelectionDialog({super.key});
@override
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
}
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
final AppDatabase _database = AppDatabase.instance;
final _searchController = TextEditingController();
List<Product> _products = [];
List<Product> _filteredProducts = [];
bool _isLoading = true;
String? _selectedCategory;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadProducts() async {
try {
final products = await _database.getProducts();
setState(() {
_products = products.where((p) => p.stock > 0).toList(); // Seulement les produits en stock
_filteredProducts = _products;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar(
'Erreur',
'Impossible de charger les produits',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredProducts = _products.where((product) {
final matchesSearch = product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false) ||
(product.imei?.toLowerCase().contains(query) ?? false);
final matchesCategory = _selectedCategory == null ||
product.category == _selectedCategory;
return matchesSearch && matchesCategory;
}).toList();
});
}
void _selectGift(Product product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.card_giftcard, color: Colors.purple.shade600),
const SizedBox(width: 8),
const Text('Confirmer le cadeau'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Produit sélectionné:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.purple.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
if (product.reference != null && product.reference!.isNotEmpty)
Text('Référence: ${product.reference}'),
if (product.category.isNotEmpty)
Text('Catégorie: ${product.category}'),
Text('Prix normal: ${product.price.toStringAsFixed(0)} MGA'),
Text('Stock disponible: ${product.stock}'),
],
),
),
const SizedBox(height: 16),
const Text(
'Ce produit sera ajouté à la commande avec un prix de 0 MGA.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context); // Fermer ce dialogue
Navigator.pop(context, ProduitCadeau(produit: product)); // Retourner le produit
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
),
child: const Text('Confirmer le cadeau'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final categories = _products.map((p) => p.category).toSet().toList()..sort();
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(20),
child: Column(
children: [
// En-tête
Row(
children: [
Icon(Icons.card_giftcard, color: Colors.purple.shade600, size: 28),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Choisir un cadeau',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
// Barre de recherche
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Rechercher un produit',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
// Filtre par catégorie
Container(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
FilterChip(
label: const Text('Toutes'),
selected: _selectedCategory == null,
onSelected: (selected) {
setState(() {
_selectedCategory = null;
_filterProducts();
});
},
),
const SizedBox(width: 8),
...categories.map((category) => Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(category),
selected: _selectedCategory == category,
onSelected: (selected) {
setState(() {
_selectedCategory = selected ? category : null;
_filterProducts();
});
},
),
)),
],
),
),
const SizedBox(height: 16),
// Liste des produits
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredProducts.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun produit disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos critères de recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
)
: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.purple.shade200),
),
child: product.image != null && product.image!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
product.image!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.image_not_supported,
color: Colors.purple.shade300),
),
)
: Icon(Icons.card_giftcard,
color: Colors.purple.shade400, size: 30),
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (product.reference != null && product.reference!.isNotEmpty)
Text('Ref: ${product.reference}'),
Text('Catégorie: ${product.category}'),
Text(
'Prix: ${product.price.toStringAsFixed(0)} MGA',
style: TextStyle(
color: Colors.green.shade600,
fontWeight: FontWeight.w600,
),
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Stock: ${product.stock}',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _selectGift(product),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Choisir', style: TextStyle(fontSize: 12)),
),
],
),
onTap: () => _selectGift(product),
),
);
},
),
),
],
),
),
);
}
}

271
lib/Components/QrScan.dart

@ -0,0 +1,271 @@
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) {
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;
}
}

996
lib/Components/appDrawer.dart

File diff suppressed because it is too large

112
lib/Components/app_bar.dart

@ -1,31 +1,121 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/controller/userController.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final Widget? subtitle;
final List<Widget>? actions;
final bool automaticallyImplyLeading;
final Color? backgroundColor;
final bool isDesktop; // Add this parameter
const CustomAppBar({
final UserController userController = Get.put(UserController());
CustomAppBar({
Key? key,
required this.title,
this.subtitle,
this.actions,
this.automaticallyImplyLeading = true,
this.backgroundColor,
this.isDesktop = false, // Add this parameter with default value
}) : super(key: key);
@override
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0);
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0);
@override
Widget build(BuildContext context) {
return AppBar(
title: subtitle == null
? Text(title)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 20)),
subtitle!,
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade900,
Colors.blue.shade800,
],
),
boxShadow: [
BoxShadow(
color: Colors.blue.shade900.withOpacity(0.3),
offset: const Offset(0, 2),
blurRadius: 4,
),
],
),
child: AppBar(
backgroundColor: backgroundColor ?? Colors.transparent,
elevation: 0,
automaticallyImplyLeading: automaticallyImplyLeading,
centerTitle: false,
iconTheme: const IconThemeData(
color: Colors.white,
size: 24,
),
actions: actions,
title: subtitle == null
? Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
),
const SizedBox(height: 2),
Obx(() => Text(
userController.role != 'Super Admin'
? 'Point de vente: ${userController.pointDeVenteDesignation}'
: '',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white.withOpacity(0.9),
letterSpacing: 0.3,
),
)),
if (subtitle != null) ...[
const SizedBox(height: 2),
DefaultTextStyle(
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.w400,
),
child: subtitle!,
),
],
],
),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade900,
Colors.blue.shade800,
],
),
// autres propriétés si besoin
),
),
),
);
}
}

11
lib/Components/colors.dart

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class AppColors {
static const primaryBlue =
Color(0xFF04365F); // Replace with your exact logo tone
static const secondaryBlue =
Color(0xFF1976D2); // Replace if your logo has a secondary tone
static const neutralGrey = Color(0xFF8A8A8A);
static const accent =
Color(0xFFF7941D); // Example: if your logo has an orange or accent
}

388
lib/Components/commandManagementComponents/CommandDetails.dart

@ -0,0 +1,388 @@
// Remplacez complètement votre fichier CommandeDetails par celui-ci :
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:intl/intl.dart';
class CommandeDetails extends StatelessWidget {
final Commande commande;
const CommandeDetails({required this.commande});
Widget _buildTableHeader(String text, {bool isAmount = false}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
textAlign: isAmount ? TextAlign.right : TextAlign.center,
),
);
}
Widget _buildTableCell(String text, {bool isAmount = false, Color? textColor}) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
text,
style: TextStyle(
fontSize: 13,
color: textColor,
),
textAlign: isAmount ? TextAlign.right : TextAlign.center,
),
);
}
Widget _buildPriceColumn(DetailCommande detail) {
if (detail.aRemise) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${detail.prixUnitaire.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 11,
decoration: TextDecoration.lineThrough,
color: Colors.grey,
),
),
const SizedBox(height: 2),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal / detail.quantite)} MGA',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
],
),
);
} else {
return _buildTableCell('${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)} MGA', isAmount: true);
}
}
Widget _buildRemiseColumn(DetailCommande detail) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: detail.aRemise
? Column(
children: [
Text(
detail.remiseDescription,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
textAlign: TextAlign.center,
),
Text(
'-${NumberFormat('#,##0', 'fr_FR').format(detail.montantRemise)} MGA',
style: TextStyle(
fontSize: 10,
color: Colors.teal.shade700,
),
textAlign: TextAlign.center,
),
],
)
: const Text(
'-',
style: TextStyle(fontSize: 13, color: Colors.grey),
textAlign: TextAlign.center,
),
);
}
Widget _buildTotalColumn(DetailCommande detail) {
if (detail.aRemise && detail.sousTotal != detail.prixFinal) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${detail.sousTotal.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 11,
decoration: TextDecoration.lineThrough,
color: Colors.grey,
),
),
const SizedBox(height: 2),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)} MGA',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
);
} else {
return _buildTableCell('${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)} MGA', isAmount: true);
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<DetailCommande>>(
future: AppDatabase.instance.getDetailsCommande(commande.id!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Text('Aucun détail disponible');
}
final details = snapshot.data!;
// Calculer les totaux
double sousTotal = 0;
double totalRemises = 0;
double totalFinal = 0;
bool hasRemises = false;
for (final detail in details) {
sousTotal += detail.sousTotal;
totalRemises += detail.montantRemise;
totalFinal += detail.prixFinal;
if (detail.aRemise) hasRemises = true;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: hasRemises ? Colors.orange.shade50 : Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: hasRemises
? Border.all(color: Colors.orange.shade200)
: null,
),
child: Row(
children: [
Icon(
hasRemises ? Icons.discount : Icons.receipt_long,
color: hasRemises ? Colors.orange.shade700 : Colors.blue.shade700,
),
const SizedBox(width: 8),
Text(
hasRemises ? 'Détails de la commande (avec remises)' : 'Détails de la commande',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: hasRemises ? Colors.orange.shade800 : Colors.black87,
),
),
if (hasRemises) ...[
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Économies: ${NumberFormat('#,##0', 'fr_FR').format(totalRemises)} MGA',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
),
],
],
),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Table(
children: [
TableRow(
decoration: BoxDecoration(
color: Colors.grey.shade100,
),
children: [
_buildTableHeader('Produit'),
_buildTableHeader('Qté'),
_buildTableHeader('Prix unit.', isAmount: true),
if (hasRemises) _buildTableHeader('Remise'),
_buildTableHeader('Total', isAmount: true),
],
),
...details.map((detail) => TableRow(
decoration: detail.aRemise
? BoxDecoration(
color: const Color.fromARGB(255, 243, 191, 114),
border: Border(
left: BorderSide(
color: Colors.orange.shade300,
width: 3,
),
),
)
: null,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
detail.produitNom ?? 'Produit inconnu',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
if (detail.aRemise) ...[
const SizedBox(height: 2),
Row(
children: [
Icon(
Icons.local_offer,
size: 12,
color: Colors.teal.shade700,
),
const SizedBox(width: 4),
Text(
'Avec remise',
style: TextStyle(
fontSize: 10,
color: Colors.teal.shade700,
fontStyle: FontStyle.italic,
),
),
],
),
],
],
),
),
_buildTableCell('${detail.quantite}'),
_buildPriceColumn(detail),
if (hasRemises) _buildRemiseColumn(detail),
_buildTotalColumn(detail),
],
)),
],
),
),
const SizedBox(height: 12),
// Section des totaux
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Column(
children: [
// Sous-total si il y a des remises
if (hasRemises) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Sous-total:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(sousTotal)} MGA',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.discount,
size: 16,
color: Colors.orange.shade700,
),
const SizedBox(width: 4),
Text(
'Remises totales:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.orange.shade700,
),
),
],
),
Text(
'-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)} MGA',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
],
),
const Divider(height: 16),
],
// Total final
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total de la commande:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Colors.green.shade700,
),
),
],
),
],
),
),
],
);
},
);
}
}

220
lib/Components/commandManagementComponents/CommandeActions.dart

@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
// Classe supplémentaire
class CommandeActions extends StatelessWidget {
final Commande commande;
final Function(int, StatutCommande) onStatutChanged;
final Function(Commande) onGenerateBonLivraison;
const CommandeActions({
required this.commande,
required this.onStatutChanged,
required this.onGenerateBonLivraison,
});
List<Widget> _buildActionButtons(BuildContext context) {
List<Widget> buttons = [];
switch (commande.statut) {
case StatutCommande.enAttente:
buttons.addAll([
// Bouton confirmer
_buildActionButton(
label: 'Confirmer',
icon: Icons.check_circle,
color: Colors.blue,
onPressed: () => _showConfirmDialog(
context,
'Confirmer la commande',
'Êtes-vous sûr de vouloir confirmer cette commande ?',
() {
// Change le statut à "confirmée"
onStatutChanged(commande.id!, StatutCommande.confirmee);
// Et génère le bon de livraison après confirmation
onGenerateBonLivraison(commande);
},
),
),
// Bouton annuler
_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.confirmee:
buttons.add(
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle,
color: Colors.green.shade600, size: 16),
const SizedBox(width: 8),
Text(
'Commande confirmée',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
break;
case StatutCommande.annulee:
buttons.add(
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cancel, color: Colors.red.shade600, size: 16),
const SizedBox(width: 8),
Text(
'Commande annulée',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
break;
}
return buttons;
}
Widget _buildActionButton({
required String label,
required IconData icon,
required Color color,
required VoidCallback onPressed,
}) {
return ElevatedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 16),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
),
);
}
void _showConfirmDialog(
BuildContext context,
String title,
String content,
VoidCallback onConfirm,
) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Row(
children: [
Icon(
Icons.help_outline,
color: Colors.blue.shade600,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(fontSize: 18),
),
],
),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Annuler',
style: TextStyle(color: Colors.grey.shade600),
),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
onConfirm();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade600,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Confirmer'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Actions sur la commande',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _buildActionButtons(context),
),
],
),
);
}
}

190
lib/Components/commandManagementComponents/DiscountDialog.dart

@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Models/client.dart';
// Dialog pour la remise
class DiscountDialog extends StatefulWidget {
final Commande commande;
const DiscountDialog({super.key, required this.commande});
@override
_DiscountDialogState createState() => _DiscountDialogState();
}
class _DiscountDialogState extends State<DiscountDialog> {
final _pourcentageController = TextEditingController();
final _montantController = TextEditingController();
bool _isPercentage = true;
double _montantFinal = 0;
@override
void initState() {
super.initState();
_montantFinal = widget.commande.montantTotal;
}
void _calculateDiscount() {
double discount = 0;
if (_isPercentage) {
final percentage = double.tryParse(_pourcentageController.text) ?? 0;
discount = (widget.commande.montantTotal * percentage) / 100;
} else {
discount = double.tryParse(_montantController.text) ?? 0;
}
setState(() {
_montantFinal = widget.commande.montantTotal - discount;
if (_montantFinal < 0) _montantFinal = 0;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Appliquer une remise'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Montant original: ${NumberFormat('#,##0', 'fr_FR').format(widget.commande.montantTotal)} MGA'),
const SizedBox(height: 16),
// Choix du type de remise
Row(
children: [
Expanded(
child: RadioListTile<bool>(
title: const Text('Pourcentage'),
value: true,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
Expanded(
child: RadioListTile<bool>(
title: const Text('Montant fixe'),
value: false,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
],
),
const SizedBox(height: 16),
if (_isPercentage)
TextField(
controller: _pourcentageController,
decoration: const InputDecoration(
labelText: 'Pourcentage de remise',
suffixText: '%',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => _calculateDiscount(),
)
else
TextField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant de remise',
suffixText: 'MGA',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => _calculateDiscount(),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant final:'),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(_montantFinal)} MGA',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
if (_montantFinal < widget.commande.montantTotal)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Économie:'),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(widget.commande.montantTotal - _montantFinal)} MGA',
style: const TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _montantFinal < widget.commande.montantTotal
? () {
final pourcentage = _isPercentage
? double.tryParse(_pourcentageController.text)
: null;
final montant = !_isPercentage
? double.tryParse(_montantController.text)
: null;
Navigator.pop(context, {
'pourcentage': pourcentage,
'montant': montant,
'montantFinal': _montantFinal,
});
}
: null,
child: const Text('Appliquer'),
),
],
);
}
@override
void dispose() {
_pourcentageController.dispose();
_montantController.dispose();
super.dispose();
}
}

136
lib/Components/commandManagementComponents/GiftSelectionDialog.dart

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
// Dialog pour sélectionner un cadeau
class GiftSelectionDialog extends StatefulWidget {
final Commande commande;
const GiftSelectionDialog({super.key, required this.commande});
@override
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
}
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
List<Product> _products = [];
List<Product> _filteredProducts = [];
final _searchController = TextEditingController();
Product? _selectedProduct;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
Future<void> _loadProducts() async {
final products = await AppDatabase.instance.getProducts();
setState(() {
_products = products.where((p) => p.stock > 0).toList();
_filteredProducts = _products;
});
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredProducts = _products.where((product) {
return product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false) ||
(product.category.toLowerCase().contains(query));
}).toList();
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Sélectionner un cadeau'),
content: SizedBox(
width: double.maxFinite,
height: 400,
child: Column(
children: [
TextField(
controller: _searchController,
decoration: const InputDecoration(
labelText: 'Rechercher un produit',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return Card(
child: ListTile(
leading: product.image != null
? Image.network(
product.image!,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.image_not_supported),
)
: const Icon(Icons.phone_android),
title: Text(product.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Catégorie: ${product.category}'),
Text('Stock: ${product.stock}'),
if (product.reference != null)
Text('Réf: ${product.reference}'),
],
),
trailing: Radio<Product>(
value: product,
groupValue: _selectedProduct,
onChanged: (value) {
setState(() {
_selectedProduct = value;
});
},
),
onTap: () {
setState(() {
_selectedProduct = product;
});
},
),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _selectedProduct != null
? () => Navigator.pop(context, _selectedProduct)
: null,
child: const Text('Ajouter le cadeau'),
),
],
);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
}

234
lib/Components/commandManagementComponents/PaswordRequired.dart

@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class PasswordVerificationDialog extends StatefulWidget {
final String title;
final String message;
final Function(String) onPasswordVerified;
const PasswordVerificationDialog({
Key? key,
required this.title,
required this.message,
required this.onPasswordVerified,
}) : super(key: key);
@override
_PasswordVerificationDialogState createState() => _PasswordVerificationDialogState();
}
class _PasswordVerificationDialogState extends State<PasswordVerificationDialog> {
final TextEditingController _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoading = false;
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
title: Row(
children: [
Icon(
Icons.security,
color: Colors.blue.shade700,
size: 28,
),
const SizedBox(width: 10),
Expanded(
child: Text(
widget.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.message,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
const SizedBox(height: 20),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: TextField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
autofocus: true,
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: Icon(
Icons.lock_outline,
color: Colors.blue.shade600,
),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility_off : Icons.visibility,
color: Colors.grey.shade600,
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (value) => _verifyPassword(),
),
),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.shade200),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: Colors.amber.shade700,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Saisissez votre mot de passe pour confirmer cette action',
style: TextStyle(
fontSize: 12,
color: Colors.amber.shade700,
),
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
child: Text(
'Annuler',
style: TextStyle(
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
),
ElevatedButton(
onPressed: _isLoading ? null : _verifyPassword,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Vérifier'),
),
],
);
}
void _verifyPassword() async {
final password = _passwordController.text.trim();
if (password.isEmpty) {
Get.snackbar(
'Erreur',
'Veuillez saisir votre mot de passe',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
return;
}
setState(() {
_isLoading = true;
});
try {
final database = AppDatabase.instance;
final isValid = await database.verifyCurrentUserPassword(password);
setState(() {
_isLoading = false;
});
if (isValid) {
Navigator.of(context).pop();
widget.onPasswordVerified(password);
} else {
Get.snackbar(
'Erreur',
'Mot de passe incorrect',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
_passwordController.clear();
}
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Une erreur est survenue lors de la vérification',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
print("Erreur vérification mot de passe: $e");
}
}
}

8
lib/Components/commandManagementComponents/PaymentMethod.dart

@ -0,0 +1,8 @@
import 'package:youmazgestion/Components/paymentType.dart';
class PaymentMethod {
final PaymentType type;
final double amountGiven;
PaymentMethod({required this.type, this.amountGiven = 0});
}

266
lib/Components/commandManagementComponents/PaymentMethodDialog.dart

@ -0,0 +1,266 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/snackbar/snackbar.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart';
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/client.dart';
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() {
final montantFinal = widget.commande.montantTotal;
if (_selectedPayment == PaymentType.cash) {
final amountGiven = double.tryParse(_amountController.text) ?? 0;
if (amountGiven < montantFinal) {
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)
: montantFinal,
));
}
@override
void initState() {
super.initState();
final montantFinal = widget.commande.montantTotal;
_amountController.text = montantFinal.toStringAsFixed(2);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final amount = double.tryParse(_amountController.text) ?? 0;
final montantFinal = widget.commande.montantTotal;
final change = amount - montantFinal;
return AlertDialog(
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Affichage du montant à payer (simplifié)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${NumberFormat('#,##0', 'fr_FR').format(montantFinal)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
),
const SizedBox(height: 16),
// 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: ${NumberFormat('#,##0', 'fr_FR').format(change)} 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),
],
),
),
),
);
}
}

7
lib/Components/commandManagementComponents/PaymentType.dart

@ -0,0 +1,7 @@
enum PaymentType {
cash,
card,
mvola,
orange,
airtel
}

412
lib/Components/newCommandComponents/CadeauDialog.dart

@ -0,0 +1,412 @@
// Components/newCommandComponents/CadeauDialog.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class CadeauDialog extends StatefulWidget {
final Product product;
final int quantite;
final DetailCommande? detailExistant;
const CadeauDialog({
Key? key,
required this.product,
required this.quantite,
this.detailExistant,
}) : super(key: key);
@override
_CadeauDialogState createState() => _CadeauDialogState();
}
class _CadeauDialogState extends State<CadeauDialog> {
final AppDatabase _database = AppDatabase.instance;
List<Product> _produitsDisponibles = [];
Product? _produitCadeauSelectionne;
int _quantiteCadeau = 1;
bool _isLoading = true;
String _searchQuery = '';
@override
void initState() {
super.initState();
_loadProduitsDisponibles();
}
Future<void> _loadProduitsDisponibles() async {
try {
final produits = await _database.getProducts();
setState(() {
_produitsDisponibles = produits.where((p) =>
p.id != widget.product.id && // Exclure le produit principal
(p.stock == null || p.stock! > 0) // Seulement les produits en stock
).toList();
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les produits: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
List<Product> get _produitsFiltres {
if (_searchQuery.isEmpty) {
return _produitsDisponibles;
}
return _produitsDisponibles.where((p) =>
p.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
(p.reference?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false)
).toList();
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.card_giftcard, color: Colors.green.shade700),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ajouter un cadeau',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
Text(
'Pour: ${widget.product.name}',
style: TextStyle(
fontSize: isMobile ? 12 : 14,
color: Colors.grey.shade600,
fontWeight: FontWeight.normal,
),
),
],
),
),
],
),
content: Container(
width: isMobile ? double.maxFinite : 500,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Information sur le produit principal
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(Icons.shopping_bag, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Produit acheté',
style: TextStyle(
fontSize: 12,
color: Colors.blue.shade700,
fontWeight: FontWeight.bold,
),
),
Text(
'${widget.quantite}x ${widget.product.name}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'Prix: ${NumberFormat('#,##0', 'fr_FR').format(widget.product.price)} MGA',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Barre de recherche
TextField(
decoration: InputDecoration(
labelText: 'Rechercher un produit cadeau',
prefixIcon: Icon(Icons.search, color: Colors.green.shade600),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.green.shade50,
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
const SizedBox(height: 16),
// Liste des produits disponibles
Expanded(
child: _produitsFiltres.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.card_giftcard_outlined,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Aucun produit disponible',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
)
: ListView.builder(
itemCount: _produitsFiltres.length,
itemBuilder: (context, index) {
final produit = _produitsFiltres[index];
final isSelected = _produitCadeauSelectionne?.id == produit.id;
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: isSelected ? 4 : 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: isSelected
? Colors.green.shade300
: Colors.grey.shade200,
width: isSelected ? 2 : 1,
),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? Colors.green.shade100
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.card_giftcard,
color: isSelected
? Colors.green.shade700
: Colors.grey.shade600,
),
),
title: Text(
produit.name,
style: TextStyle(
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Prix normal: ${NumberFormat('#,##0', 'fr_FR').format(produit.price)} MGA',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
decoration: TextDecoration.lineThrough,
),
),
Row(
children: [
Icon(
Icons.card_giftcard,
size: 14,
color: Colors.green.shade600,
),
const SizedBox(width: 4),
Text(
'GRATUIT',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
if (produit.stock != null)
Text(
'Stock: ${produit.stock}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
),
),
],
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: Colors.green.shade700,
)
: null,
onTap: () {
setState(() {
_produitCadeauSelectionne = produit;
});
},
),
);
},
),
),
// Sélection de la quantité si un produit est sélectionné
if (_produitCadeauSelectionne != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Icon(Icons.card_giftcard, color: Colors.green.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
'Quantité de ${_produitCadeauSelectionne!.name}',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.green.shade700,
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.green.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove, size: 16),
onPressed: _quantiteCadeau > 1
? () {
setState(() {
_quantiteCadeau--;
});
}
: null,
),
Text(
_quantiteCadeau.toString(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
IconButton(
icon: const Icon(Icons.add, size: 16),
onPressed: () {
final maxStock = _produitCadeauSelectionne!.stock ?? 99;
if (_quantiteCadeau < maxStock) {
setState(() {
_quantiteCadeau++;
});
}
},
),
],
),
),
],
),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 20,
vertical: isMobile ? 10 : 12,
),
),
icon: const Icon(Icons.card_giftcard),
label: Text(
isMobile ? 'Offrir' : 'Offrir le cadeau',
style: TextStyle(fontSize: isMobile ? 12 : 14),
),
onPressed: _produitCadeauSelectionne != null
? () {
Get.back(result: {
'produit': _produitCadeauSelectionne!,
'quantite': _quantiteCadeau,
});
}
: null,
),
],
);
}
}

332
lib/Components/newCommandComponents/RemiseDialog.dart

@ -0,0 +1,332 @@
// Components/RemiseDialog.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
class RemiseDialog extends StatefulWidget {
final Product product;
final int quantite;
final double prixUnitaire;
final DetailCommande? detailExistant;
const RemiseDialog({
super.key,
required this.product,
required this.quantite,
required this.prixUnitaire,
this.detailExistant,
});
@override
State<RemiseDialog> createState() => _RemiseDialogState();
}
class _RemiseDialogState extends State<RemiseDialog> {
final _formKey = GlobalKey<FormState>();
final _valeurController = TextEditingController();
RemiseType _selectedType = RemiseType.pourcentage;
double _montantRemise = 0.0;
double _prixFinal = 0.0;
late double _sousTotal;
@override
void initState() {
super.initState();
_sousTotal = widget.quantite * widget.prixUnitaire;
// Si on modifie une remise existante
if (widget.detailExistant?.aRemise == true) {
_selectedType = widget.detailExistant!.remiseType!;
_valeurController.text = widget.detailExistant!.remiseValeur.toString();
_calculateRemise();
} else {
_prixFinal = _sousTotal;
}
}
void _calculateRemise() {
final valeur = double.tryParse(_valeurController.text) ?? 0.0;
setState(() {
if (_selectedType == RemiseType.pourcentage) {
final pourcentage = valeur.clamp(0.0, 100.0);
_montantRemise = _sousTotal * (pourcentage / 100);
} else {
_montantRemise = valeur.clamp(0.0, _sousTotal);
}
_prixFinal = _sousTotal - _montantRemise;
});
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.discount, color: Colors.orange.shade700),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Appliquer une remise',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
),
],
),
content: Container(
width: isMobile ? double.maxFinite : 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informations du produit
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Quantité: ${widget.quantite}',
style: const TextStyle(fontSize: 12),
),
Text(
'Prix unitaire: ${NumberFormat('#,##0', 'fr_FR').format(widget.prixUnitaire)} MGA',
style: const TextStyle(fontSize: 12),
),
Text(
'Sous-total: ${NumberFormat('#,##0', 'fr_FR').format(_sousTotal)} MGA',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
// Type de remise
const Text(
'Type de remise:',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RadioListTile<RemiseType>(
title: const Text('Pourcentage (%)', style: TextStyle(fontSize: 12)),
value: RemiseType.pourcentage,
groupValue: _selectedType,
onChanged: (value) {
setState(() {
_selectedType = value!;
_calculateRemise();
});
},
contentPadding: EdgeInsets.zero,
dense: true,
),
),
Expanded(
child: RadioListTile<RemiseType>(
title: const Text('Montant (MGA)', style: TextStyle(fontSize: 12)),
value: RemiseType.montant,
groupValue: _selectedType,
onChanged: (value) {
setState(() {
_selectedType = value!;
_calculateRemise();
});
},
contentPadding: EdgeInsets.zero,
dense: true,
),
),
],
),
const SizedBox(height: 16),
// Valeur de la remise
TextFormField(
controller: _valeurController,
decoration: InputDecoration(
labelText: _selectedType == RemiseType.pourcentage
? 'Pourcentage (0-100)'
: 'Montant en MGA',
prefixIcon: Icon(
_selectedType == RemiseType.pourcentage
? Icons.percent
: Icons.attach_money,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer une valeur';
}
final valeur = double.tryParse(value);
if (valeur == null || valeur < 0) {
return 'Valeur invalide';
}
if (_selectedType == RemiseType.pourcentage && valeur > 100) {
return 'Le pourcentage ne peut pas dépasser 100%';
}
if (_selectedType == RemiseType.montant && valeur > _sousTotal) {
return 'La remise ne peut pas dépasser le sous-total';
}
return null;
},
onChanged: (value) => _calculateRemise(),
),
const SizedBox(height: 16),
// Aperçu du calcul
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Sous-total:', style: TextStyle(fontSize: 12)),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(_sousTotal)} MGA',
style: const TextStyle(fontSize: 12),
),
],
),
if (_montantRemise > 0) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Remise ${_selectedType == RemiseType.pourcentage ? "(${_valeurController.text}%)" : ""}:',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
),
),
Text(
'-${NumberFormat('#,##0', 'fr_FR').format(_montantRemise)} MGA',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
],
const Divider(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Prix final:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'${NumberFormat('#,##0', 'fr_FR').format(_prixFinal)} MGA',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
),
),
],
),
],
),
),
],
),
),
),
actions: [
if (widget.detailExistant?.aRemise == true)
TextButton.icon(
onPressed: () => Navigator.of(context).pop('supprimer'),
icon: const Icon(Icons.delete, color: Colors.red),
label: const Text('Supprimer remise', style: TextStyle(color: Colors.red)),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
final valeur = double.parse(_valeurController.text);
Navigator.of(context).pop({
'type': _selectedType,
'valeur': valeur,
'montantRemise': _montantRemise,
'prixFinal': _prixFinal,
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade700,
foregroundColor: Colors.white,
),
child: const Text('Appliquer'),
),
],
);
}
@override
void dispose() {
_valeurController.dispose();
super.dispose();
}
}

7
lib/Components/paymentType.dart

@ -0,0 +1,7 @@
enum PaymentType {
cash,
card,
mvola,
orange,
airtel
}

831
lib/Components/windows_qr_scanner.dart

@ -0,0 +1,831 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
import '../Models/produit.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class DemandeSortiePersonnellePage extends StatefulWidget {
const DemandeSortiePersonnellePage({super.key});
@override
_DemandeSortiePersonnellePageState createState() =>
_DemandeSortiePersonnellePageState();
}
class _DemandeSortiePersonnellePageState
extends State<DemandeSortiePersonnellePage> with TickerProviderStateMixin {
final AppDatabase _database = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
final _formKey = GlobalKey<FormState>();
final _quantiteController = TextEditingController(text: '1');
final _motifController = TextEditingController();
final _notesController = TextEditingController();
final _searchController = TextEditingController();
Product? _selectedProduct;
List<Product> _products = [];
List<Product> _filteredProducts = [];
bool _isLoading = false;
bool _isSearching = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
);
_loadProducts();
_searchController.addListener(_filterProducts);
}
void _scanQrOrBarcode() async {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Container(
width: double.maxFinite,
height: 400,
child: MobileScanner(
onDetect: (BarcodeCapture barcodeCap) {
print("BarcodeCapture: $barcodeCap");
// Now accessing the barcodes attribute
final List<Barcode> barcodes = barcodeCap.barcodes;
if (barcodes.isNotEmpty) {
// Get the first detected barcode value
String? scanResult = barcodes.first.rawValue;
print("Scanned Result: $scanResult");
if (scanResult != null && scanResult.isNotEmpty) {
setState(() {
_searchController.text = scanResult;
print(
"Updated Search Controller: ${_searchController.text}");
});
// Close dialog after scanning
Navigator.of(context).pop();
// Refresh product list based on new search input
_filterProducts();
} else {
print("Scan result was empty or null.");
Navigator.of(context).pop();
}
} else {
print("No barcodes detected.");
Navigator.of(context).pop();
}
},
),
),
);
},
);
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
if (query.isEmpty) {
_filteredProducts = _products;
_isSearching = false;
} else {
_isSearching = true;
_filteredProducts = _products.where((product) {
return product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false);
}).toList();
}
});
}
Future<void> _loadProducts() async {
setState(() => _isLoading = true);
try {
final products = await _database.getProducts();
setState(() {
_products = products.where((p) {
// Check stock availability
print("point de vente id: ${_userController.pointDeVenteId}");
bool hasStock = _userController.pointDeVenteId == 0
? (p.stock ?? 0) > 0
: (p.stock ?? 0) > 0 &&
p.pointDeVenteId == _userController.pointDeVenteId;
return hasStock;
}).toList();
// Setting filtered products
_filteredProducts = _products;
// End loading state
_isLoading = false;
});
// Start the animation
_animationController.forward();
} catch (e) {
// Handle any errors
setState(() {
_isLoading = false;
});
_showErrorSnackbar('Impossible de charger les produits: $e');
}
}
Future<void> _soumettreDemandePersonnelle() async {
if (!_formKey.currentState!.validate() || _selectedProduct == null) {
_showErrorSnackbar('Veuillez remplir tous les champs obligatoires');
return;
}
final quantite = int.tryParse(_quantiteController.text) ?? 0;
if (quantite <= 0) {
_showErrorSnackbar('La quantité doit être supérieure à 0');
return;
}
if ((_selectedProduct!.stock ?? 0) < quantite) {
_showErrorSnackbar(
'Stock insuffisant (disponible: ${_selectedProduct!.stock})');
return;
}
// Confirmation dialog
final confirmed = await _showConfirmationDialog();
if (!confirmed) return;
setState(() => _isLoading = true);
try {
await _database.createSortieStockPersonnelle(
produitId: _selectedProduct!.id!,
adminId: _userController.userId,
quantite: quantite,
motif: _motifController.text.trim(),
pointDeVenteId: _userController.pointDeVenteId > 0
? _userController.pointDeVenteId
: null,
notes: _notesController.text.trim().isNotEmpty
? _notesController.text.trim()
: null,
);
_showSuccessSnackbar(
'Votre demande de sortie personnelle a été soumise pour approbation');
// Réinitialiser le formulaire avec animation
_resetForm();
_loadProducts();
} catch (e) {
_showErrorSnackbar('Impossible de soumettre la demande: $e');
} finally {
setState(() => _isLoading = false);
}
}
void _resetForm() {
_formKey.currentState!.reset();
_quantiteController.text = '1';
_motifController.clear();
_notesController.clear();
_searchController.clear();
setState(() {
_selectedProduct = null;
_isSearching = false;
});
}
Future<bool> _showConfirmationDialog() async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.help_outline, color: Colors.orange.shade700),
const SizedBox(width: 8),
const Text('Confirmer la demande'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Êtes-vous sûr de vouloir soumettre cette demande ?'),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Produit: ${_selectedProduct?.name}'),
Text('Quantité: ${_quantiteController.text}'),
Text('Motif: ${_motifController.text}'),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade700,
foregroundColor: Colors.white,
),
child: const Text('Confirmer'),
),
],
),
) ??
false;
}
void _showSuccessSnackbar(String message) {
Get.snackbar(
'',
'',
titleText: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
const Text('Succès',
style:
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
messageText: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: Colors.green.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 4),
margin: const EdgeInsets.all(16),
borderRadius: 12,
icon: Icon(Icons.check_circle_outline, color: Colors.white),
);
}
void _showErrorSnackbar(String message) {
Get.snackbar(
'',
'',
titleText: Row(
children: [
Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
const Text('Erreur',
style:
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
messageText: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: Colors.red.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 4),
margin: const EdgeInsets.all(16),
borderRadius: 12,
);
}
Widget _buildHeaderCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade600, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.shade200,
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.inventory_2, color: Colors.white, size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sortie personnelle de stock',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'Demande d\'approbation requise',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Cette fonctionnalité permet aux administrateurs de demander '
'la sortie d\'un produit du stock pour usage personnel. '
'Toute demande nécessite une approbation avant traitement.',
style: TextStyle(fontSize: 14, color: Colors.white),
),
),
],
),
);
}
Widget _buildProductSelector() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sélection du produit *',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 12),
// Barre de recherche
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher un produit...',
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600),
),
onChanged: (value) {
_filterProducts(); // Call to filter products
},
),
),
IconButton(
icon: Icon(Icons.qr_code_scanner, color: Colors.blue),
onPressed: _scanQrOrBarcode,
tooltip: 'Scanner QR ou code-barres',
),
],
),
const SizedBox(height: 12),
// Liste des produits
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: _filteredProducts.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off,
size: 48, color: Colors.grey.shade400),
const SizedBox(height: 8),
Text(
_isSearching
? 'Aucun produit trouvé'
: 'Aucun produit disponible',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
)
: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
final isSelected = _selectedProduct?.id == product.id;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isSelected
? Colors.orange.shade50
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Colors.orange.shade300
: Colors.transparent,
width: 2,
),
),
child: ListTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isSelected
? Colors.orange.shade100
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.inventory,
color: isSelected
? Colors.orange.shade700
: Colors.grey.shade600,
),
),
title: Text(
product.name,
style: TextStyle(
fontWeight:
isSelected ? FontWeight.bold : FontWeight.w500,
color: isSelected
? Colors.orange.shade800
: Colors.grey.shade800,
),
),
subtitle: Text(
'Stock: ${product.stock} • Réf: ${product.reference ?? 'N/A'}',
style: TextStyle(
color: isSelected
? Colors.orange.shade600
: Colors.grey.shade600,
),
),
trailing: isSelected
? Icon(Icons.check_circle,
color: Colors.orange.shade700)
: Icon(Icons.radio_button_unchecked,
color: Colors.grey.shade400),
onTap: () {
setState(() {
_selectedProduct = product;
});
},
),
);
},
),
),
],
);
}
Widget _buildFormSection() {
return Column(
children: [
// Quantité
_buildInputField(
label: 'Quantité *',
controller: _quantiteController,
keyboardType: TextInputType.number,
icon: Icons.format_list_numbered,
suffix: _selectedProduct != null
? Text('max: ${_selectedProduct!.stock}',
style: TextStyle(color: Colors.grey.shade600))
: null,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer une quantité';
}
final quantite = int.tryParse(value);
if (quantite == null || quantite <= 0) {
return 'Quantité invalide';
}
if (_selectedProduct != null &&
quantite > (_selectedProduct!.stock ?? 0)) {
return 'Quantité supérieure au stock disponible';
}
return null;
},
),
const SizedBox(height: 20),
// Motif
_buildInputField(
label: 'Motif *',
controller: _motifController,
icon: Icons.description,
hintText: 'Raison de cette sortie personnelle',
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Veuillez indiquer le motif';
}
if (value.trim().length < 5) {
return 'Le motif doit contenir au moins 5 caractères';
}
return null;
},
),
const SizedBox(height: 20),
// Notes
_buildInputField(
label: 'Notes complémentaires',
controller: _notesController,
icon: Icons.note_add,
hintText: 'Informations complémentaires (optionnel)',
maxLines: 3,
),
],
);
}
Widget _buildInputField({
required String label,
required TextEditingController controller,
required IconData icon,
String? hintText,
TextInputType? keyboardType,
int maxLines = 1,
Widget? suffix,
String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
validator: validator,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: Icon(icon, color: Colors.grey.shade600),
suffix: suffix,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.orange.shade400, width: 2),
),
filled: true,
fillColor: Colors.grey.shade50,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
],
);
}
Widget _buildUserInfoCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person, color: Colors.grey.shade700),
const SizedBox(width: 8),
Text(
'Informations de la demande',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
],
),
const SizedBox(height: 12),
_buildInfoRow(
Icons.account_circle, 'Demandeur', _userController.name),
if (_userController.pointDeVenteId > 0)
_buildInfoRow(Icons.store, 'Point de vente',
_userController.pointDeVenteDesignation),
_buildInfoRow(Icons.calendar_today, 'Date',
DateTime.now().toLocal().toString().split(' ')[0]),
],
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text(
'$label: ',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
Expanded(
child: Text(
value,
style: TextStyle(color: Colors.grey.shade800),
),
),
],
),
);
}
Widget _buildSubmitButton() {
return Container(
width: double.infinity,
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.orange.shade700, Colors.orange.shade500],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.orange.shade300,
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: _isLoading ? null : _soumettreDemandePersonnelle,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: _isLoading
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
SizedBox(width: 12),
Text(
'Traitement...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.send, color: Colors.white),
SizedBox(width: 8),
Text(
'Soumettre la demande',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Demande sortie personnelle'),
drawer: CustomDrawer(),
body: _isLoading && _products.isEmpty
? const Center(child: CircularProgressIndicator())
: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderCard(),
const SizedBox(height: 24),
_buildProductSelector(),
const SizedBox(height: 24),
_buildFormSection(),
const SizedBox(height: 24),
_buildUserInfoCard(),
const SizedBox(height: 32),
_buildSubmitButton(),
const SizedBox(height: 16),
],
),
),
),
),
),
);
}
@override
void dispose() {
_animationController.dispose();
_quantiteController.dispose();
_motifController.dispose();
_notesController.dispose();
_searchController.dispose();
super.dispose();
}
}
extension on BarcodeCapture {
get rawValue => null;
}

366
lib/Models/Client.dart

@ -1,4 +1,4 @@
// Models/client.dart
// Models/client.dart - Version corrigée pour MySQL
class Client {
final int? id;
final String nom;
@ -33,29 +33,49 @@ class Client {
};
}
// Fonction helper améliorée pour parser les dates
static DateTime _parseDateTime(dynamic dateValue) {
if (dateValue == null) return DateTime.now();
if (dateValue is DateTime) return dateValue;
if (dateValue is String) {
try {
return DateTime.parse(dateValue);
} catch (e) {
print("Erreur parsing date string: $dateValue, erreur: $e");
return DateTime.now();
}
}
// Pour MySQL qui peut retourner un Timestamp
if (dateValue is int) {
return DateTime.fromMillisecondsSinceEpoch(dateValue);
}
print("Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue");
return DateTime.now();
}
factory Client.fromMap(Map<String, dynamic> map) {
return Client(
id: map['id'],
nom: map['nom'],
prenom: map['prenom'],
email: map['email'],
telephone: map['telephone'],
adresse: map['adresse'],
dateCreation: DateTime.parse(map['dateCreation']),
actif: map['actif'] == 1,
id: map['id'] as int?,
nom: map['nom'] as String,
prenom: map['prenom'] as String,
email: map['email'] as String,
telephone: map['telephone'] as String,
adresse: map['adresse'] as String?,
dateCreation: _parseDateTime(map['dateCreation']),
actif: (map['actif'] as int?) == 1,
);
}
String get nomComplet => '$prenom $nom';
}
// Models/commande.dart
enum StatutCommande {
enAttente,
confirmee,
enPreparation,
expediee,
livree,
annulee
}
@ -67,25 +87,53 @@ class Commande {
final double montantTotal;
final String? notes;
final DateTime? dateLivraison;
// Données du client (pour les jointures)
final int? commandeurId;
final String? commandeurnom;
final String? commandeurPrenom;
final int? validateurId;
final String? clientNom;
final String? clientPrenom;
final String? clientEmail;
final int? pointDeVenteId;
final String? pointDeVenteDesign;
Commande({
this.id,
required this.clientId,
required this.dateCommande,
this.statut = StatutCommande.enAttente,
required this.statut,
required this.montantTotal,
this.notes,
this.dateLivraison,
this.commandeurId,
this.commandeurnom,
this.commandeurPrenom,
this.validateurId,
this.clientNom,
this.clientPrenom,
this.clientEmail,
this.pointDeVenteId,
this.pointDeVenteDesign,
});
String get clientNomComplet {
if (clientNom != null && clientPrenom != null) {
return '$clientPrenom $clientNom';
}
return 'Client inconnu';
}
String get statutLibelle {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.annulee:
return 'Annulée';
}
}
Map<String, dynamic> toMap() {
return {
'id': id,
@ -95,59 +143,53 @@ class Commande {
'montantTotal': montantTotal,
'notes': notes,
'dateLivraison': dateLivraison?.toIso8601String(),
'commandeurId': commandeurId,
'validateurId': validateurId,
};
}
factory Commande.fromMap(Map<String, dynamic> map) {
return Commande(
id: map['id'],
clientId: map['clientId'],
dateCommande: DateTime.parse(map['dateCommande']),
statut: StatutCommande.values[map['statut']],
montantTotal: map['montantTotal'].toDouble(),
notes: map['notes'],
id: map['id'] as int?,
clientId: map['clientId'] as int,
dateCommande: Client._parseDateTime(map['dateCommande']),
statut: StatutCommande.values[(map['statut'] as int)],
montantTotal: (map['montantTotal'] as num).toDouble(),
notes: map['notes'] as String?,
dateLivraison: map['dateLivraison'] != null
? DateTime.parse(map['dateLivraison'])
? Client._parseDateTime(map['dateLivraison'])
: null,
clientNom: map['clientNom'],
clientPrenom: map['clientPrenom'],
clientEmail: map['clientEmail'],
commandeurId: map['commandeurId'] as int?,
commandeurnom: map['commandeurnom'] as String?,
commandeurPrenom: map['commandeurPrenom'] as String?,
validateurId: map['validateurId'] as int?,
clientNom: map['clientNom'] as String?,
clientPrenom: map['clientPrenom'] as String?,
clientEmail: map['clientEmail'] as String?,
pointDeVenteId: map['pointDeVenteId'] as int?,
pointDeVenteDesign: map['pointDeVenteDesign'] as String?,
);
}
}
String get statutLibelle {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.enPreparation:
return 'En préparation';
case StatutCommande.expediee:
return 'Expédiée';
case StatutCommande.livree:
return 'Livrée';
case StatutCommande.annulee:
return 'Annulée';
}
}
String get clientNomComplet =>
clientPrenom != null && clientNom != null
? '$clientPrenom $clientNom'
: 'Client inconnu';
// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
enum RemiseType {
pourcentage,
montant
}
// Models/detail_commande.dart
class DetailCommande {
final int? id;
final int commandeId;
final int produitId;
final int quantite;
final double prixUnitaire;
final double sousTotal;
// Données du produit (pour les jointures)
final double sousTotal; // Prix unitaire × quantité (avant remise)
final RemiseType? remiseType;
final double remiseValeur; // Valeur de la remise (% ou montant)
final double montantRemise; // Montant de la remise calculé
final double prixFinal; // Prix final après remise
final bool estCadeau; // NOUVEAU : Indique si l'article est un cadeau
final String? produitNom;
final String? produitImage;
final String? produitReference;
@ -159,11 +201,195 @@ class DetailCommande {
required this.quantite,
required this.prixUnitaire,
required this.sousTotal,
this.remiseType,
this.remiseValeur = 0.0,
this.montantRemise = 0.0,
required this.prixFinal,
this.estCadeau = false,
this.produitNom,
this.produitImage,
this.produitReference,
});
// Constructeur pour créer un détail sans remise
factory DetailCommande.sansRemise({
int? id,
required int commandeId,
required int produitId,
required int quantite,
required double prixUnitaire,
bool estCadeau = false,
String? produitNom,
String? produitImage,
String? produitReference,
}) {
final sousTotal = quantite * prixUnitaire;
final prixFinal = estCadeau ? 0.0 : sousTotal;
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
prixFinal: prixFinal,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Constructeur pour créer un cadeau
factory DetailCommande.cadeau({
int? id,
required int commandeId,
required int produitId,
required int quantite,
required double prixUnitaire,
String? produitNom,
String? produitImage,
String? produitReference,
}) {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: quantite * prixUnitaire,
prixFinal: 0.0, // Prix final à 0 pour un cadeau
estCadeau: true,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Méthode pour appliquer une remise (ne s'applique pas aux cadeaux)
DetailCommande appliquerRemise({
required RemiseType type,
required double valeur,
}) {
// Les remises ne s'appliquent pas aux cadeaux
if (estCadeau) return this;
double montantRemiseCalcule = 0.0;
if (type == RemiseType.pourcentage) {
final pourcentage = valeur.clamp(0.0, 100.0);
montantRemiseCalcule = sousTotal * (pourcentage / 100);
} else {
montantRemiseCalcule = valeur.clamp(0.0, sousTotal);
}
final prixFinalCalcule = sousTotal - montantRemiseCalcule;
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: type,
remiseValeur: valeur,
montantRemise: montantRemiseCalcule,
prixFinal: prixFinalCalcule,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Méthode pour supprimer la remise
DetailCommande supprimerRemise() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: null,
remiseValeur: 0.0,
montantRemise: 0.0,
prixFinal: estCadeau ? 0.0 : sousTotal,
estCadeau: estCadeau,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Méthode pour convertir en cadeau
DetailCommande convertirEnCadeau() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: null, // Supprimer les remises lors de la conversion en cadeau
remiseValeur: 0.0,
montantRemise: 0.0,
prixFinal: 0.0,
estCadeau: true,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// NOUVEAU : Méthode pour convertir en article normal
DetailCommande convertirEnArticleNormal() {
return DetailCommande(
id: id,
commandeId: commandeId,
produitId: produitId,
quantite: quantite,
prixUnitaire: prixUnitaire,
sousTotal: sousTotal,
remiseType: remiseType,
remiseValeur: remiseValeur,
montantRemise: montantRemise,
prixFinal: estCadeau ? sousTotal - montantRemise : prixFinal,
estCadeau: false,
produitNom: produitNom,
produitImage: produitImage,
produitReference: produitReference,
);
}
// Getters utiles
bool get aRemise => remiseType != null && montantRemise > 0 && !estCadeau;
double get pourcentageRemise {
if (!aRemise) return 0.0;
return (montantRemise / sousTotal) * 100;
}
String get remiseDescription {
if (estCadeau) return 'CADEAU';
if (!aRemise) return '';
if (remiseType == RemiseType.pourcentage) {
return '-${remiseValeur.toStringAsFixed(0)}%';
} else {
return '-${montantRemise.toStringAsFixed(2)} MGA';
}
}
// NOUVEAU : Description du statut de l'article
String get statutDescription {
if (estCadeau) return 'CADEAU OFFERT';
if (aRemise) return 'AVEC REMISE';
return 'PRIX NORMAL';
}
Map<String, dynamic> toMap() {
return {
'id': id,
@ -172,20 +398,40 @@ class DetailCommande {
'quantite': quantite,
'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal,
'remise_type': remiseType?.name,
'remise_valeur': remiseValeur,
'montant_remise': montantRemise,
'prix_final': prixFinal,
'est_cadeau': estCadeau ? 1 : 0,
};
}
factory DetailCommande.fromMap(Map<String, dynamic> map) {
RemiseType? type;
if (map['remise_type'] != null) {
if (map['remise_type'] == 'pourcentage') {
type = RemiseType.pourcentage;
} else if (map['remise_type'] == 'montant') {
type = RemiseType.montant;
}
}
return DetailCommande(
id: map['id'],
commandeId: map['commandeId'],
produitId: map['produitId'],
quantite: map['quantite'],
prixUnitaire: map['prixUnitaire'].toDouble(),
sousTotal: map['sousTotal'].toDouble(),
produitNom: map['produitNom'],
produitImage: map['produitImage'],
produitReference: map['produitReference'],
id: map['id'] as int?,
commandeId: map['commandeId'] as int,
produitId: map['produitId'] as int,
quantite: map['quantite'] as int,
prixUnitaire: (map['prixUnitaire'] as num).toDouble(),
sousTotal: (map['sousTotal'] as num).toDouble(),
remiseType: type,
remiseValeur: (map['remise_valeur'] as num?)?.toDouble() ?? 0.0,
montantRemise: (map['montant_remise'] as num?)?.toDouble() ?? 0.0,
prixFinal: (map['prix_final'] as num?)?.toDouble() ??
(map['sousTotal'] as num).toDouble(),
estCadeau: (map['est_cadeau'] as int?) == 1,
produitNom: map['produitNom'] as String?,
produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'] as String?,
);
}
}

64
lib/Models/Remise.dart

@ -0,0 +1,64 @@
import 'package:youmazgestion/Components/paymentType.dart';
import 'package:youmazgestion/Models/produit.dart';
class Remise {
final RemiseType type;
final double valeur;
final String description;
Remise({
required this.type,
required this.valeur,
this.description = '',
});
double calculerRemise(double montantOriginal) {
switch (type) {
case RemiseType.pourcentage:
return montantOriginal * (valeur / 100);
case RemiseType.fixe:
return valeur;
}
}
String get libelle {
switch (type) {
case RemiseType.pourcentage:
return '$valeur%';
case RemiseType.fixe:
return '${valeur.toStringAsFixed(0)} MGA';
}
}
}
enum RemiseType { pourcentage, fixe }
class ProduitCadeau {
final Product produit;
final String motif;
ProduitCadeau({
required this.produit,
this.motif = 'Cadeau client',
});
}
// Modifiez votre classe PaymentMethod pour inclure la remise
class PaymentMethodEnhanced {
final PaymentType type;
final double amountGiven;
final Remise? remise;
PaymentMethodEnhanced({
required this.type,
this.amountGiven = 0,
this.remise,
});
double calculerMontantFinal(double montantOriginal) {
if (remise != null) {
return montantOriginal - remise!.calculerRemise(montantOriginal);
}
return montantOriginal;
}
}

36
lib/Models/pointage_model.dart

@ -0,0 +1,36 @@
class Pointage {
final int? id;
final String userName;
final String date;
final String heureArrivee;
final String heureDepart;
Pointage({
this.id,
required this.userName,
required this.date,
required this.heureArrivee,
required this.heureDepart,
});
// Pour SQLite
factory Pointage.fromMap(Map<String, dynamic> map) {
return Pointage(
id: map['id'],
userName: map['userName'] ?? '',
date: map['date'],
heureArrivee: map['heureArrivee'],
heureDepart: map['heureDepart'],
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'userName': userName,
'date': date,
'heureArrivee': heureArrivee,
'heureDepart': heureDepart,
};
}
}

170
lib/Models/produit.dart

@ -1,13 +1,23 @@
// Models/product.dart - Version corrigée pour gérer les Blobs
import 'dart:typed_data';
import 'dart:convert';
class Product {
int? id;
final int? id;
final String name;
final double price;
final String? image;
final String category;
final int? stock;
final int stock;
final String? description;
String? qrCode;
String? qrCode;
final String? reference;
final int? pointDeVenteId;
final String? pointDeVentelib;
final String? marque;
final String? ram;
final String? memoireInterne;
final String? imei;
Product({
this.id,
@ -16,44 +26,140 @@ class Product {
this.image,
required this.category,
this.stock = 0,
this.description = '',
this.description,
this.qrCode,
this.reference,
this.pointDeVenteId,
this.pointDeVentelib,
this.marque,
this.ram,
this.memoireInterne,
this.imei,
});
// Vérifie si le stock est défini
bool isStockDefined() {
if (stock != null) {
print("stock is defined : $stock $name");
return true;
} else {
return false;
return stock > 0;
}
// Méthode helper pour convertir de façon sécurisée
static String? _convertImageFromMap(dynamic imageValue) {
if (imageValue == null) {
return null;
}
// Si c'est déjà une String, on la retourne
if (imageValue is String) {
return imageValue;
}
// Le driver mysql1 peut retourner un Blob même pour TEXT
// Essayer de le convertir en String
try {
if (imageValue is Uint8List) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
if (imageValue is List<int>) {
// Convertir les bytes en String UTF-8
return utf8.decode(imageValue);
}
// Dernier recours : toString()
return imageValue.toString();
} catch (e) {
print("Erreur conversion image: $e, type: ${imageValue.runtimeType}");
return null;
}
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'price': price,
'image': image ?? '',
'category': category,
'stock': stock ?? 0,
'description': description ?? '',
'qrCode': qrCode ?? '',
'reference': reference ?? '',
};
factory Product.fromMap(Map<String, dynamic> map) => Product(
id: map['id'] as int?,
name: map['name'] as String,
price: (map['price'] as num).toDouble(), // Conversion sécurisée
image: _convertImageFromMap(map['image']), // Utilisation de la méthode helper
category: map['category'] as String,
stock: (map['stock'] as int?) ?? 0, // Valeur par défaut
description: map['description'] as String?,
qrCode: map['qrCode'] as String?,
reference: map['reference'] as String?,
pointDeVenteId: map['point_de_vente_id'] as int?,
pointDeVentelib: map['pointDeVentelib'] as String?,
marque: map['marque'] as String?,
ram: map['ram'] as String?,
memoireInterne: map['memoire_interne'] as String?,
imei: map['imei'] as String?,
);
Map<String, dynamic> toMap() => {
'id': id,
'name': name,
'price': price,
'image': image,
'category': category,
'stock': stock,
'description': description,
'qrCode': qrCode,
'reference': reference,
'point_de_vente_id': pointDeVenteId,
'marque': marque,
'ram': ram,
'memoire_interne': memoireInterne,
'imei': imei,
};
// Méthode pour obtenir l'image comme base64 si nécessaire
String? getImageAsBase64() {
if (image == null) return null;
// Si l'image est déjà en base64, la retourner
if (image!.startsWith('data:') || image!.length > 100) {
return image;
}
// Sinon, c'est probablement un chemin de fichier
return image;
}
// Méthode pour vérifier si l'image est un base64
bool get isImageBase64 {
if (image == null) return false;
return image!.startsWith('data:') ||
(image!.length > 100 && !image!.contains('/') && !image!.contains('\\'));
}
factory Product.fromMap(Map<String, dynamic> map) {
// Copie avec modification
Product copyWith({
int? id,
String? name,
double? price,
String? image,
String? category,
int? stock,
String? description,
String? qrCode,
String? reference,
int? pointDeVenteId,
String? marque,
String? ram,
String? memoireInterne,
String? imei,
}) {
return Product(
id: map['id'],
name: map['name'],
price: map['price'],
image: map['image'],
category: map['category'],
stock: map['stock'],
description: map['description'],
qrCode: map['qrCode'],
reference: map['reference'],
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
image: image ?? this.image,
category: category ?? this.category,
stock: stock ?? this.stock,
description: description ?? this.description,
qrCode: qrCode ?? this.qrCode,
reference: reference ?? this.reference,
pointDeVenteId: pointDeVenteId ?? this.pointDeVenteId,
marque: marque ?? this.marque,
ram: ram ?? this.ram,
memoireInterne: memoireInterne ?? this.memoireInterne,
imei: imei ?? this.imei,
);
}
}

33
lib/Models/users.dart

@ -1,3 +1,4 @@
// 1. Models/users.dart - Version corrigée
class Users {
int? id;
String name;
@ -6,7 +7,8 @@ class Users {
String password;
String username;
int roleId;
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN
String? roleName;
int? pointDeVenteId;
Users({
this.id,
@ -17,38 +19,43 @@ class Users {
required this.username,
required this.roleId,
this.roleName,
this.pointDeVenteId,
});
// CORRIGÉ: Méthode toMap() qui correspond exactement aux colonnes de la DB
Map<String, dynamic> toMap() {
return {
'id': id, // Inclure l'ID pour les updates
'name': name,
'lastname': lastName,
'lastname': lastName, // Correspond à la colonne DB
'email': email,
'password': password,
'username': username,
'role_id': roleId,
'point_de_vente_id': pointDeVenteId,
};
}
Map<String, dynamic> toMapWithId() {
// Méthode pour créer un map sans l'ID (pour les insertions)
Map<String, dynamic> toMapForInsert() {
final map = toMap();
if (id != null) map['id'] = id;
map.remove('id');
return map;
}
factory Users.fromMap(Map<String, dynamic> map) {
return Users(
id: map['id'],
name: map['name'],
lastName: map['lastname'],
email: map['email'],
password: map['password'],
username: map['username'],
roleId: map['role_id'],
roleName: map['role_name'], // Depuis les requêtes avec JOIN
id: map['id'] as int?,
name: map['name'] as String,
lastName: map['lastname'] as String, // Correspond à la colonne DB
email: map['email'] as String,
password: map['password'] as String,
username: map['username'] as String,
roleId: map['role_id'] as int,
roleName: map['role_name'] as String?, // Depuis les JOINs
pointDeVenteId: map['point_de_vente_id'] as int?,
);
}
// Getter pour la compatibilité avec l'ancien code
String get role => roleName ?? '';
}

0
lib/Services/GestionStockDatabase.dart

258
lib/Services/PermissionCacheService.dart

@ -0,0 +1,258 @@
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class PermissionCacheService extends GetxController {
static final PermissionCacheService instance = PermissionCacheService._init();
PermissionCacheService._init();
// Cache en mémoire optimisé
final Map<String, Map<String, bool>> _permissionCache = {};
final Map<String, List<Map<String, dynamic>>> _menuCache = {};
bool _isLoaded = false;
String _currentUsername = '';
/// OPTIMISÉ: Une seule requête complexe pour charger tout
Future<void> loadUserPermissions(String username) async {
if (_isLoaded && _currentUsername == username && _permissionCache.containsKey(username)) {
print("📋 Permissions déjà en cache pour: $username");
return;
}
print("🔄 Chargement OPTIMISÉ des permissions pour: $username");
final stopwatch = Stopwatch()..start();
try {
final db = AppDatabase.instance;
// 🚀 UNE SEULE REQUÊTE pour tout récupérer
final userPermissions = await _getUserPermissionsOptimized(db, username);
// Organiser les données
Map<String, bool> permissions = {};
Set<Map<String, dynamic>> accessibleMenus = {};
for (var row in userPermissions) {
final menuId = row['menu_id'] as int;
final menuName = row['menu_name'] as String;
final menuRoute = row['menu_route'] as String;
final permissionName = row['permission_name'] as String;
// Ajouter la permission
final key = "${permissionName}_$menuRoute";
permissions[key] = true;
// Ajouter le menu aux accessibles
accessibleMenus.add({
'id': menuId,
'name': menuName,
'route': menuRoute,
});
}
// Mettre en cache
_permissionCache[username] = permissions;
_menuCache[username] = accessibleMenus.toList();
_currentUsername = username;
_isLoaded = true;
stopwatch.stop();
print("✅ Permissions chargées en ${stopwatch.elapsedMilliseconds}ms");
print(" - ${permissions.length} permissions");
print(" - ${accessibleMenus.length} menus accessibles");
} catch (e) {
stopwatch.stop();
print("❌ Erreur après ${stopwatch.elapsedMilliseconds}ms: $e");
rethrow;
}
}
/// 🚀 NOUVELLE MÉTHODE: Une seule requête optimisée
Future<List<Map<String, dynamic>>> _getUserPermissionsOptimized(
AppDatabase db, String username) async {
final connection = await db.database;
final result = await connection.query('''
SELECT DISTINCT
m.id as menu_id,
m.name as menu_name,
m.route as menu_route,
p.name as permission_name
FROM users u
INNER JOIN roles r ON u.role_id = r.id
INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id
INNER JOIN menu m ON rmp.menu_id = m.id
INNER JOIN permissions p ON rmp.permission_id = p.id
WHERE u.username = ?
ORDER BY m.name, p.name
''', [username]);
return result.map((row) => row.fields).toList();
}
/// Vérification rapide depuis le cache
bool hasPermission(String username, String permissionName, String menuRoute) {
final userPermissions = _permissionCache[username];
if (userPermissions == null) {
print("⚠️ Cache non initialisé pour: $username");
return false;
}
final key = "${permissionName}_$menuRoute";
return userPermissions[key] ?? false;
}
/// Récupération rapide des menus
List<Map<String, dynamic>> getUserMenus(String username) {
return _menuCache[username] ?? [];
}
/// Vérification d'accès menu
bool hasMenuAccess(String username, String menuRoute) {
final userMenus = _menuCache[username] ?? [];
return userMenus.any((menu) => menu['route'] == menuRoute);
}
/// Préchargement asynchrone en arrière-plan
Future<void> preloadUserDataAsync(String username) async {
// Lancer en arrière-plan sans bloquer l'UI
unawaited(_preloadInBackground(username));
}
Future<void> _preloadInBackground(String username) async {
try {
print("🔄 Préchargement en arrière-plan pour: $username");
await loadUserPermissions(username);
print("✅ Préchargement terminé");
} catch (e) {
print("⚠️ Erreur préchargement: $e");
}
}
/// Préchargement synchrone (pour la connexion)
Future<void> preloadUserData(String username) async {
try {
print("🔄 Préchargement synchrone pour: $username");
await loadUserPermissions(username);
print("✅ Données préchargées avec succès");
} catch (e) {
print("❌ Erreur lors du préchargement: $e");
// Ne pas bloquer la connexion
}
}
/// Vider le cache
void clearAllCache() {
_permissionCache.clear();
_menuCache.clear();
_isLoaded = false;
_currentUsername = '';
print("🗑️ Cache vidé complètement");
}
/// Rechargement forcé
Future<void> refreshUserPermissions(String username) async {
_permissionCache.remove(username);
_menuCache.remove(username);
_isLoaded = false;
await loadUserPermissions(username);
print("🔄 Permissions rechargées pour: $username");
}
/// Status du cache
bool get isLoaded => _isLoaded && _currentUsername.isNotEmpty;
String get currentCachedUser => _currentUsername;
/// Statistiques
Map<String, dynamic> getCacheStats() {
return {
'is_loaded': _isLoaded,
'current_user': _currentUsername,
'users_cached': _permissionCache.length,
'total_permissions': _permissionCache.values
.map((perms) => perms.length)
.fold(0, (a, b) => a + b),
'total_menus': _menuCache.values
.map((menus) => menus.length)
.fold(0, (a, b) => a + b),
};
}
/// Debug amélioré
void debugPrintCache() {
print("=== DEBUG CACHE OPTIMISÉ ===");
print("Chargé: $_isLoaded");
print("Utilisateur actuel: $_currentUsername");
print("Utilisateurs en cache: ${_permissionCache.keys.toList()}");
for (var username in _permissionCache.keys) {
final permissions = _permissionCache[username]!;
final menus = _menuCache[username] ?? [];
print("$username: ${permissions.length} permissions, ${menus.length} menus");
// Détail des menus pour debug
for (var menu in menus.take(3)) {
print("${menu['name']} (${menu['route']})");
}
}
print("============================");
}
/// NOUVEAU: Validation de l'intégrité du cache
Future<bool> validateCacheIntegrity(String username) async {
if (!_permissionCache.containsKey(username)) {
return false;
}
try {
final db = AppDatabase.instance;
final connection = await db.database;
// Vérification rapide: compter les permissions de l'utilisateur
final result = await connection.query('''
SELECT COUNT(DISTINCT CONCAT(p.name, '_', m.route)) as permission_count
FROM users u
INNER JOIN roles r ON u.role_id = r.id
INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id
INNER JOIN menu m ON rmp.menu_id = m.id
INNER JOIN permissions p ON rmp.permission_id = p.id
WHERE u.username = ?
''', [username]);
final dbCount = result.first['permission_count'] as int;
final cacheCount = _permissionCache[username]!.length;
final isValid = dbCount == cacheCount;
if (!isValid) {
print("⚠️ Cache invalide: DB=$dbCount, Cache=$cacheCount");
}
return isValid;
} catch (e) {
print("❌ Erreur validation cache: $e");
return false;
}
}
/// NOUVEAU: Rechargement intelligent
Future<void> smartRefresh(String username) async {
final isValid = await validateCacheIntegrity(username);
if (!isValid) {
print("🔄 Cache invalide, rechargement nécessaire");
await refreshUserPermissions(username);
} else {
print("✅ Cache valide, pas de rechargement nécessaire");
}
}
}
/// Extension pour éviter l'import de dart:async
void unawaited(Future future) {
// Ignorer le warning sur le Future non attendu
future.catchError((error) {
print("Erreur tâche en arrière-plan: $error");
});
}

359
lib/Services/Script.sql

@ -0,0 +1,359 @@
-- Script SQL pour créer la base de données guycom_database_v1
-- Création des tables et insertion des données par défaut
-- =====================================================
-- CRÉATION DES TABLES
-- =====================================================
-- Table permissions
CREATE TABLE `permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table menu
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`route` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table roles
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`designation` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `designation` (`designation`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table points_de_vente
CREATE TABLE `points_de_vente` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nom` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `nom` (`nom`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table clients
CREATE TABLE `clients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nom` varchar(255) NOT NULL,
`prenom` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`telephone` varchar(255) NOT NULL,
`adresse` varchar(500) DEFAULT NULL,
`dateCreation` datetime NOT NULL,
`actif` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
KEY `idx_clients_email` (`email`),
KEY `idx_clients_telephone` (`telephone`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table users
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`lastname` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`username` varchar(255) NOT NULL,
`role_id` int(11) NOT NULL,
`point_de_vente_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `username` (`username`),
KEY `role_id` (`role_id`),
KEY `point_de_vente_id` (`point_de_vente_id`),
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`),
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table products
CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`price` decimal(10,2) NOT NULL,
`image` varchar(2000) DEFAULT NULL,
`category` varchar(255) NOT NULL,
`stock` int(11) NOT NULL DEFAULT 0,
`description` varchar(1000) DEFAULT NULL,
`qrCode` varchar(500) DEFAULT NULL,
`reference` varchar(255) DEFAULT NULL,
`point_de_vente_id` int(11) DEFAULT NULL,
`marque` varchar(255) DEFAULT NULL,
`ram` varchar(100) DEFAULT NULL,
`memoire_interne` varchar(100) DEFAULT NULL,
`imei` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `imei` (`imei`),
KEY `point_de_vente_id` (`point_de_vente_id`),
KEY `idx_products_category` (`category`),
KEY `idx_products_reference` (`reference`),
KEY `idx_products_imei` (`imei`),
CONSTRAINT `products_ibfk_1` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table commandes
CREATE TABLE `commandes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`clientId` int(11) NOT NULL,
`dateCommande` datetime NOT NULL,
`statut` int(11) NOT NULL DEFAULT 0,
`montantTotal` decimal(10,2) NOT NULL,
`notes` varchar(1000) DEFAULT NULL,
`dateLivraison` datetime DEFAULT NULL,
`commandeurId` int(11) DEFAULT NULL,
`validateurId` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `commandeurId` (`commandeurId`),
KEY `validateurId` (`validateurId`),
KEY `idx_commandes_client` (`clientId`),
KEY `idx_commandes_date` (`dateCommande`),
CONSTRAINT `commandes_ibfk_1` FOREIGN KEY (`commandeurId`) REFERENCES `users` (`id`),
CONSTRAINT `commandes_ibfk_2` FOREIGN KEY (`validateurId`) REFERENCES `users` (`id`),
CONSTRAINT `commandes_ibfk_3` FOREIGN KEY (`clientId`) REFERENCES `clients` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table details_commandes
CREATE TABLE `details_commandes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commandeId` int(11) NOT NULL,
`produitId` int(11) NOT NULL,
`quantite` int(11) NOT NULL,
`prixUnitaire` decimal(10,2) NOT NULL,
`sousTotal` decimal(10,2) NOT NULL,
`remise_type` enum('pourcentage','montant') DEFAULT NULL,
`remise_valeur` decimal(10,2) DEFAULT 0.00,
`montant_remise` decimal(10,2) DEFAULT 0.00,
`prix_final` decimal(10,2) NOT NULL DEFAULT 0.00,
`est_cadeau` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `produitId` (`produitId`),
KEY `idx_details_commande` (`commandeId`),
KEY `idx_est_cadeau` (`est_cadeau`),
CONSTRAINT `details_commandes_ibfk_1` FOREIGN KEY (`commandeId`) REFERENCES `commandes` (`id`) ON DELETE CASCADE,
CONSTRAINT `details_commandes_ibfk_2` FOREIGN KEY (`produitId`) REFERENCES `products` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table role_permissions
CREATE TABLE `role_permissions` (
`role_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`),
KEY `permission_id` (`permission_id`),
CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table role_menu_permissions
CREATE TABLE `role_menu_permissions` (
`role_id` int(11) NOT NULL,
`menu_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL,
PRIMARY KEY (`role_id`,`menu_id`,`permission_id`),
KEY `menu_id` (`menu_id`),
KEY `permission_id` (`permission_id`),
CONSTRAINT `role_menu_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_menu_permissions_ibfk_2` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_menu_permissions_ibfk_3` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- =====================================================
-- INSERTION DES DONNÉES PAR DÉFAUT
-- =====================================================
-- Insertion des permissions par défaut
INSERT INTO `permissions` (`name`) VALUES
('view'),
('create'),
('update'),
('delete'),
('admin'),
('manage'),
('read');
-- Insertion des menus par défaut
INSERT INTO `menu` (`name`, `route`) VALUES
('Accueil', '/accueil'),
('Ajouter un utilisateur', '/ajouter-utilisateur'),
('Modifier/Supprimer un utilisateur', '/modifier-utilisateur'),
('Ajouter un produit', '/ajouter-produit'),
('Modifier/Supprimer un produit', '/modifier-produit'),
('Bilan', '/bilan'),
('Gérer les rôles', '/gerer-roles'),
('Gestion de stock', '/gestion-stock'),
('Historique', '/historique'),
('Déconnexion', '/deconnexion'),
('Nouvelle commande', '/nouvelle-commande'),
('Gérer les commandes', '/gerer-commandes'),
('Points de vente', '/points-de-vente');
-- Insertion des rôles par défaut
INSERT INTO `roles` (`designation`) VALUES
('Super Admin'),
('Admin'),
('User'),
('commercial'),
('caisse');
-- Attribution de TOUTES les permissions à TOUS les menus pour le Super Admin
-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'Super Admin';
-- Attribution de permissions basiques pour Admin
-- Accès en lecture/écriture à la plupart des menus sauf gestion des rôles
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'Admin'
AND m.name != 'Gérer les rôles'
AND p.name IN ('view', 'create', 'update', 'read');
-- Attribution de permissions basiques pour User
-- Accès principalement en lecture et quelques actions de base
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'User'
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Gestion de stock', 'Historique')
AND p.name IN ('view', 'read', 'create');
-- Attribution de permissions pour Commercial
-- Accès aux commandes, clients, produits
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'commercial'
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gérer les commandes', 'Bilan', 'Historique')
AND p.name IN ('view', 'create', 'update', 'read');
-- Attribution de permissions pour Caisse
-- Accès principalement aux commandes et stock
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`, `permission_id`)
SELECT r.id, m.id, p.id
FROM menu m
CROSS JOIN permissions p
CROSS JOIN roles r
WHERE r.designation = 'caisse'
AND m.name IN ('Accueil', 'Nouvelle commande', 'Gestion de stock')
AND p.name IN ('view', 'create', 'read');
-- Insertion du Super Admin par défaut
-- On utilise une sous-requête pour récupérer l'ID réel du rôle Super Admin
INSERT INTO `users` (`name`, `lastname`, `email`, `password`, `username`, `role_id`)
SELECT 'Super', 'Admin', 'superadmin@youmazgestion.com', 'admin123', 'superadmin', r.id
FROM roles r
WHERE r.designation = 'Super Admin';
-- =====================================================
-- DONNÉES D'EXEMPLE (OPTIONNEL)
-- =====================================================
-- Insertion d'un point de vente d'exemple
INSERT INTO `points_de_vente` (`nom`) VALUES ('Magasin Principal');
-- Insertion d'un client d'exemple
INSERT INTO `clients` (`nom`, `prenom`, `email`, `telephone`, `adresse`, `dateCreation`, `actif`) VALUES
('Dupont', 'Jean', 'jean.dupont@email.com', '0123456789', '123 Rue de la Paix, Paris', NOW(), 1);
-- =====================================================
-- VÉRIFICATIONS
-- =====================================================
-- Afficher les rôles créés
SELECT 'RÔLES CRÉÉS:' as info;
SELECT * FROM roles;
-- Afficher les permissions créées
SELECT 'PERMISSIONS CRÉÉES:' as info;
SELECT * FROM permissions;
-- Afficher les menus créés
SELECT 'MENUS CRÉÉS:' as info;
SELECT * FROM menu;
-- Afficher le Super Admin créé
SELECT 'SUPER ADMIN CRÉÉ:' as info;
SELECT u.username, u.email, r.designation as role
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE r.designation = 'Super Admin';
-- Vérifier les permissions du Super Admin
SELECT 'PERMISSIONS SUPER ADMIN:' as info;
SELECT COUNT(*) as total_permissions_assignees
FROM role_menu_permissions rmp
INNER JOIN roles r ON rmp.role_id = r.id
WHERE r.designation = 'Super Admin';
SELECT 'Script terminé avec succès!' as resultat;
CREATE TABLE `demandes_transfert` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`produit_id` int(11) NOT NULL,
`point_de_vente_source_id` int(11) NOT NULL,
`point_de_vente_destination_id` int(11) NOT NULL,
`demandeur_id` int(11) NOT NULL,
`validateur_id` int(11) DEFAULT NULL,
`quantite` int(11) NOT NULL DEFAULT 1,
`statut` enum('en_attente','validee','refusee') NOT NULL DEFAULT 'en_attente',
`date_demande` datetime NOT NULL,
`date_validation` datetime DEFAULT NULL,
`notes` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `produit_id` (`produit_id`),
KEY `point_de_vente_source_id` (`point_de_vente_source_id`),
KEY `point_de_vente_destination_id` (`point_de_vente_destination_id`),
KEY `demandeur_id` (`demandeur_id`),
KEY `validateur_id` (`validateur_id`),
CONSTRAINT `demandes_transfert_ibfk_1` FOREIGN KEY (`produit_id`) REFERENCES `products` (`id`),
CONSTRAINT `demandes_transfert_ibfk_2` FOREIGN KEY (`point_de_vente_source_id`) REFERENCES `points_de_vente` (`id`),
CONSTRAINT `demandes_transfert_ibfk_3` FOREIGN KEY (`point_de_vente_destination_id`) REFERENCES `points_de_vente` (`id`),
CONSTRAINT `demandes_transfert_ibfk_4` FOREIGN KEY (`demandeur_id`) REFERENCES `users` (`id`),
CONSTRAINT `demandes_transfert_ibfk_5` FOREIGN KEY (`validateur_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Table pour tracer les sorties de stock personnelles
CREATE TABLE `sorties_stock_personnelles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`produit_id` int(11) NOT NULL,
`admin_id` int(11) NOT NULL,
`quantite` int(11) NOT NULL DEFAULT 1,
`motif` varchar(500) NOT NULL,
`date_sortie` datetime NOT NULL,
`point_de_vente_id` int(11) DEFAULT NULL,
`notes` text DEFAULT NULL,
`statut` enum('en_attente','approuvee','refusee') NOT NULL DEFAULT 'en_attente',
`approbateur_id` int(11) DEFAULT NULL,
`date_approbation` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `produit_id` (`produit_id`),
KEY `admin_id` (`admin_id`),
KEY `point_de_vente_id` (`point_de_vente_id`),
KEY `approbateur_id` (`approbateur_id`),
CONSTRAINT `sorties_personnelles_ibfk_1` FOREIGN KEY (`produit_id`) REFERENCES `products` (`id`),
CONSTRAINT `sorties_personnelles_ibfk_2` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`),
CONSTRAINT `sorties_personnelles_ibfk_3` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`),
CONSTRAINT `sorties_personnelles_ibfk_4` FOREIGN KEY (`approbateur_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

680
lib/Services/app_database.dart

@ -1,680 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import '../Models/users.dart';
import '../Models/role.dart';
import '../Models/Permission.dart';
class AppDatabase {
static final AppDatabase instance = AppDatabase._init();
late Database _database;
AppDatabase._init() {
sqfliteFfiInit();
}
Future<Database> get database async {
if (_database.isOpen) return _database;
_database = await _initDB('app_database.db');
return _database;
}
Future<void> initDatabase() async {
_database = await _initDB('app_database.db');
await _createDB(_database, 1);
await insertDefaultPermissions();
await insertDefaultMenus();
await insertDefaultRoles();
await insertDefaultSuperAdmin();
}
Future<Database> _initDB(String filePath) async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, filePath);
bool dbExists = await File(path).exists();
if (!dbExists) {
try {
ByteData data = await rootBundle.load('assets/database/$filePath');
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await File(path).writeAsBytes(bytes);
} catch (e) {
print('Pas de fichier DB dans assets, création d\'une nouvelle DB');
}
}
return await databaseFactoryFfi.openDatabase(path);
}
Future<void> _createDB(Database db, int version) async {
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
final tableNames = tables.map((row) => row['name'] as String).toList();
if (!tableNames.contains('roles')) {
await db.execute('''
CREATE TABLE roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
designation TEXT NOT NULL UNIQUE
)
''');
print("Table 'roles' créée.");
}
if (!tableNames.contains('permissions')) {
await db.execute('''
CREATE TABLE permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
''');
print("Table 'permissions' créée.");
}
if (!tableNames.contains('menu')) {
await db.execute('''
CREATE TABLE menu (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
route TEXT NOT NULL UNIQUE
)
''');
print("Table 'menu' créée.");
}
if (!tableNames.contains('role_permissions')) {
await db.execute('''
CREATE TABLE role_permissions (
role_id INTEGER,
permission_id INTEGER,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
)
''');
print("Table 'role_permissions' créée.");
}
if (!tableNames.contains('menu_permissions')) {
await db.execute('''
CREATE TABLE menu_permissions (
menu_id INTEGER,
permission_id INTEGER,
PRIMARY KEY (menu_id, permission_id),
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
)
''');
print("Table 'menu_permissions' créée.");
}
if (!tableNames.contains('users')) {
await db.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
lastname TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
role_id INTEGER NOT NULL,
FOREIGN KEY (role_id) REFERENCES roles(id)
)
''');
print("Table 'users' créée.");
}
if (!tableNames.contains('role_menu_permissions')) {
await db.execute('''
CREATE TABLE role_menu_permissions (
role_id INTEGER,
menu_id INTEGER,
permission_id INTEGER,
PRIMARY KEY (role_id, menu_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
)
''');
print("Table 'role_menu_permissions' créée.");
}
}
Future<void> insertDefaultPermissions() async {
final db = await database;
final existing = await db.query('permissions');
if (existing.isEmpty) {
await db.insert('permissions', {'name': 'view'});
await db.insert('permissions', {'name': 'create'});
await db.insert('permissions', {'name': 'update'});
await db.insert('permissions', {'name': 'delete'});
await db.insert('permissions', {'name': 'admin'});
await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission
await db.insert('permissions', {'name': 'read'}); // Nouvelle permission
print("Permissions par défaut insérées");
} else {
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas
final newPermissions = ['manage', 'read'];
for (var permission in newPermissions) {
final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]);
if (existingPermission.isEmpty) {
await db.insert('permissions', {'name': permission});
print("Permission ajoutée: $permission");
}
}
}
}
Future<void> insertDefaultMenus() async {
final db = await database;
final existingMenus = await db.query('menu');
if (existingMenus.isEmpty) {
// Menus existants
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'});
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'});
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'});
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'});
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'});
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'});
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'});
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'});
await db.insert('menu', {'name': 'Historique', 'route': '/historique'});
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'});
// Nouveaux menus ajoutés
await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'});
await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'});
print("Menus par défaut insérés");
} else {
// Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants
await _addMissingMenus(db);
}
}
Future<void> _addMissingMenus(Database db) async {
final menusToAdd = [
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
];
for (var menu in menusToAdd) {
final existing = await db.query(
'menu',
where: 'route = ?',
whereArgs: [menu['route']],
);
if (existing.isEmpty) {
await db.insert('menu', menu);
print("Menu ajouté: ${menu['name']}");
}
}
}
Future<void> insertDefaultRoles() async {
final db = await database;
final existingRoles = await db.query('roles');
if (existingRoles.isEmpty) {
int superAdminRoleId = await db.insert('roles', {'designation': 'Super Admin'});
int adminRoleId = await db.insert('roles', {'designation': 'Admin'});
int userRoleId = await db.insert('roles', {'designation': 'User'});
final permissions = await db.query('permissions');
final menus = await db.query('menu');
// Assigner toutes les permissions à tous les menus pour le Super Admin
for (var menu in menus) {
for (var permission in permissions) {
await db.insert('role_menu_permissions', {
'role_id': superAdminRoleId,
'menu_id': menu['id'],
'permission_id': permission['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
}
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId);
print("Rôles par défaut créés et permissions assignées");
} else {
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes
await _updateExistingRolePermissions(db);
}
}
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus
Future<void> _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async {
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']);
final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']);
final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']);
final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']);
// Récupérer les IDs des nouveaux menus
final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']);
final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']);
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) {
// Admin peut créer de nouvelles commandes
await db.insert('role_menu_permissions', {
'role_id': adminRoleId,
'menu_id': nouvelleCommandeMenu.first['id'],
'permission_id': createPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
// User peut aussi créer de nouvelles commandes
await db.insert('role_menu_permissions', {
'role_id': userRoleId,
'menu_id': nouvelleCommandeMenu.first['id'],
'permission_id': createPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) {
// Admin peut gérer les commandes
await db.insert('role_menu_permissions', {
'role_id': adminRoleId,
'menu_id': gererCommandesMenu.first['id'],
'permission_id': managePermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) {
// User peut voir les commandes
await db.insert('role_menu_permissions', {
'role_id': userRoleId,
'menu_id': gererCommandesMenu.first['id'],
'permission_id': viewPermission.first['id'],
}
, conflictAlgorithm: ConflictAlgorithm.ignore
);
}
}
Future<void> _updateExistingRolePermissions(Database db) async {
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
if (superAdminRole.isNotEmpty) {
final superAdminRoleId = superAdminRole.first['id'] as int;
final permissions = await db.query('permissions');
final menus = await db.query('menu');
// Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus
for (var menu in menus) {
for (var permission in permissions) {
final existingPermission = await db.query(
'role_menu_permissions',
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
whereArgs: [superAdminRoleId, menu['id'], permission['id']],
);
if (existingPermission.isEmpty) {
await db.insert('role_menu_permissions', {
'role_id': superAdminRoleId,
'menu_id': menu['id'],
'permission_id': permission['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
}
}
}
// Assigner les permissions de base aux autres rôles pour les nouveaux menus
final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']);
final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']);
if (adminRole.isNotEmpty && userRole.isNotEmpty) {
await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int);
}
print("Permissions mises à jour pour tous les rôles");
}
}
Future<void> insertDefaultSuperAdmin() async {
final db = await database;
final existingSuperAdmin = await db.rawQuery('''
SELECT u.* FROM users u
INNER JOIN roles r ON u.role_id = r.id
WHERE r.designation = 'Super Admin'
''');
if (existingSuperAdmin.isEmpty) {
final superAdminRole = await db.query('roles',
where: 'designation = ?',
whereArgs: ['Super Admin']
);
if (superAdminRole.isNotEmpty) {
final superAdminRoleId = superAdminRole.first['id'] as int;
await db.insert('users', {
'name': 'Super',
'lastname': 'Admin',
'email': 'superadmin@youmazgestion.com',
'password': 'admin123',
'username': 'superadmin',
'role_id': superAdminRoleId,
});
print("Super Admin créé avec succès !");
print("Username: superadmin");
print("Password: admin123");
print("ATTENTION: Changez ce mot de passe après la première connexion !");
}
} else {
print("Super Admin existe déjà");
}
}
Future<int> createUser(Users user) async {
final db = await database;
return await db.insert('users', user.toMap());
}
Future<int> deleteUser(int id) async {
final db = await database;
return await db.delete('users', where: 'id = ?', whereArgs: [id]);
}
Future<int> updateUser(Users user) async {
final db = await database;
return await db.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]);
}
Future<int> getUserCount() async {
final db = await database;
List<Map<String, dynamic>> result = await db.rawQuery('SELECT COUNT(*) as count FROM users');
return result.first['count'] as int;
}
Future<bool> verifyUser(String username, String password) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.id
FROM users
WHERE users.username = ? AND users.password = ?
''', [username, password]);
return result.isNotEmpty;
}
Future<Users> getUser(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.*, roles.designation as role_name
FROM users
INNER JOIN roles ON users.role_id = roles.id
WHERE users.username = ?
''', [username]);
if (result.isNotEmpty) {
return Users.fromMap(result.first);
} else {
throw Exception('User not found');
}
}
Future<Map<String, dynamic>?> getUserCredentials(String username, String password) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id
FROM users
INNER JOIN roles ON users.role_id = roles.id
WHERE username = ? AND password = ?
''', [username, password]);
if (result.isNotEmpty) {
return {
'id': result.first['id'],
'username': result.first['username'] as String,
'role': result.first['role_name'] as String,
'role_id': result.first['role_id'],
};
} else {
return null;
}
}
Future<List<Users>> getAllUsers() async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.*, roles.designation as role_name
FROM users
INNER JOIN roles ON users.role_id = roles.id
ORDER BY users.id ASC
''');
return result.map((json) => Users.fromMap(json)).toList();
}
Future<int> createRole(Role role) async {
final db = await database;
return await db.insert('roles', role.toMap());
}
Future<List<Role>> getRoles() async {
final db = await database;
final maps = await db.query('roles', orderBy: 'designation ASC');
return List.generate(maps.length, (i) => Role.fromMap(maps[i]));
}
Future<int> updateRole(Role role) async {
final db = await database;
return await db.update(
'roles',
role.toMap(),
where: 'id = ?',
whereArgs: [role.id],
);
}
Future<int> deleteRole(int? id) async {
final db = await database;
return await db.delete(
'roles',
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<Permission>> getAllPermissions() async {
final db = await database;
final result = await db.query('permissions', orderBy: 'name ASC');
return result.map((e) => Permission.fromMap(e)).toList();
}
Future<List<Permission>> getPermissionsForRole(int roleId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT p.id, p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?
ORDER BY p.name ASC
''', [roleId]);
return result.map((map) => Permission.fromMap(map)).toList();
}
Future<List<Permission>> getPermissionsForUser(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT DISTINCT p.id, p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN roles r ON rp.role_id = r.id
JOIN users u ON u.role_id = r.id
WHERE u.username = ?
ORDER BY p.name ASC
''', [username]);
return result.map((map) => Permission.fromMap(map)).toList();
}
Future<void> assignPermission(int roleId, int permissionId) async {
final db = await database;
await db.insert('role_permissions', {
'role_id': roleId,
'permission_id': permissionId,
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removePermission(int roleId, int permissionId) async {
final db = await database;
await db.delete(
'role_permissions',
where: 'role_id = ? AND permission_id = ?',
whereArgs: [roleId, permissionId],
);
}
Future<void> assignMenuPermission(int menuId, int permissionId) async {
final db = await database;
await db.insert('menu_permissions', {
'menu_id': menuId,
'permission_id': permissionId,
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removeMenuPermission(int menuId, int permissionId) async {
final db = await database;
await db.delete(
'menu_permissions',
where: 'menu_id = ? AND permission_id = ?',
whereArgs: [menuId, permissionId],
);
}
Future<bool> isSuperAdmin(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(*) as count
FROM users u
INNER JOIN roles r ON u.role_id = r.id
WHERE u.username = ? AND r.designation = 'Super Admin'
''', [username]);
return (result.first['count'] as int) > 0;
}
Future<void> changePassword(String username, String oldPassword, String newPassword) async {
final db = await database;
final isValidOldPassword = await verifyUser(username, oldPassword);
if (!isValidOldPassword) {
throw Exception('Ancien mot de passe incorrect');
}
await db.update(
'users',
{'password': newPassword},
where: 'username = ?',
whereArgs: [username],
);
}
Future<bool> hasPermission(String username, String permissionName, String menuRoute) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(*) as count
FROM permissions p
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id
JOIN roles r ON rmp.role_id = r.id
JOIN users u ON u.role_id = r.id
JOIN menu m ON m.route = ?
WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id
''', [menuRoute, username, permissionName]);
return (result.first['count'] as int) > 0;
}
Future<void> close() async {
if (_database.isOpen) {
await _database.close();
}
}
Future<void> printDatabaseInfo() async {
final db = await database;
print("=== INFORMATIONS DE LA BASE DE DONNÉES ===");
final userCount = await getUserCount();
print("Nombre d'utilisateurs: $userCount");
final users = await getAllUsers();
print("Utilisateurs:");
for (var user in users) {
print(" - ${user.username} (${user.name} ) - Email: ${user.email}");
}
final roles = await getRoles();
print("Rôles:");
for (var role in roles) {
print(" - ${role.designation} (ID: ${role.id})");
}
final permissions = await getAllPermissions();
print("Permissions:");
for (var permission in permissions) {
print(" - ${permission.name} (ID: ${permission.id})");
}
print("=========================================");
}
Future<List<Permission>> getPermissionsForRoleAndMenu(int roleId, int menuId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT p.id, p.name
FROM permissions p
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id
WHERE rmp.role_id = ? AND rmp.menu_id = ?
ORDER BY p.name ASC
''', [roleId, menuId]);
return result.map((map) => Permission.fromMap(map)).toList();
}
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue
Future<void> deleteDatabaseFile() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'app_database.db');
final file = File(path);
if (await file.exists()) {
await file.delete();
print("Base de données utilisateur supprimée");
}
}
Future<void> assignRoleMenuPermission(int roleId, int menuId, int permissionId) async {
final db = await database;
await db.insert('role_menu_permissions', {
'role_id': roleId,
'menu_id': menuId,
'permission_id': permissionId,
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removeRoleMenuPermission(int roleId, int menuId, int permissionId) async {
final db = await database;
await db.delete(
'role_menu_permissions',
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
whereArgs: [roleId, menuId, permissionId],
);
}
}

559
lib/Services/productDatabase.dart

@ -1,559 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import '../Models/produit.dart';
import '../Models/client.dart';
class ProductDatabase {
static final ProductDatabase instance = ProductDatabase._init();
late Database _database;
ProductDatabase._init() {
sqfliteFfiInit();
}
ProductDatabase();
Future<Database> get database async {
if (_database.isOpen) return _database;
_database = await _initDB('products2.db');
return _database;
}
Future<void> initDatabase() async {
_database = await _initDB('products2.db');
await _createDB(_database, 1);
await _insertDefaultClients();
await _insertDefaultCommandes();
}
Future<Database> _initDB(String filePath) async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, filePath);
bool dbExists = await File(path).exists();
if (!dbExists) {
try {
ByteData data = await rootBundle.load('assets/database/$filePath');
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await File(path).writeAsBytes(bytes);
} catch (e) {
print('Pas de fichier DB dans assets, création nouvelle DB');
}
}
return await databaseFactoryFfi.openDatabase(path);
}
Future<void> _createDB(Database db, int version) async {
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
final tableNames = tables.map((row) => row['name'] as String).toList();
// Table products (existante avec améliorations)
if (!tableNames.contains('products')) {
await db.execute('''
CREATE TABLE products(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
image TEXT,
category TEXT NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
description TEXT,
qrCode TEXT,
reference TEXT UNIQUE
)
''');
print("Table 'products' créée.");
} else {
// Vérifier et ajouter les colonnes manquantes
await _updateProductsTable(db);
}
// Table clients
if (!tableNames.contains('clients')) {
await db.execute('''
CREATE TABLE clients(
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
prenom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
telephone TEXT NOT NULL,
adresse TEXT,
dateCreation TEXT NOT NULL,
actif INTEGER NOT NULL DEFAULT 1
)
''');
print("Table 'clients' créée.");
}
// Table commandes
if (!tableNames.contains('commandes')) {
await db.execute('''
CREATE TABLE commandes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
clientId INTEGER NOT NULL,
dateCommande TEXT NOT NULL,
statut INTEGER NOT NULL DEFAULT 0,
montantTotal REAL NOT NULL,
notes TEXT,
dateLivraison TEXT,
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE
)
''');
print("Table 'commandes' créée.");
}
// Table détails commandes
if (!tableNames.contains('details_commandes')) {
await db.execute('''
CREATE TABLE details_commandes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
commandeId INTEGER NOT NULL,
produitId INTEGER NOT NULL,
quantite INTEGER NOT NULL,
prixUnitaire REAL NOT NULL,
sousTotal REAL NOT NULL,
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
FOREIGN KEY (produitId) REFERENCES products(id) ON DELETE CASCADE
)
''');
print("Table 'details_commandes' créée.");
}
// Créer les index pour optimiser les performances
await _createIndexes(db);
}
Future<void> _updateProductsTable(Database db) async {
final columns = await db.rawQuery('PRAGMA table_info(products)');
final columnNames = columns.map((e) => e['name'] as String).toList();
if (!columnNames.contains('description')) {
await db.execute("ALTER TABLE products ADD COLUMN description TEXT");
print("Colonne 'description' ajoutée.");
}
if (!columnNames.contains('qrCode')) {
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT");
print("Colonne 'qrCode' ajoutée.");
}
if (!columnNames.contains('reference')) {
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT");
print("Colonne 'reference' ajoutée.");
}
}
Future<void> _createIndexes(Database db) async {
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_reference ON products(reference)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_client ON commandes(clientId)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_date ON commandes(dateCommande)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_details_commande ON details_commandes(commandeId)');
print("Index créés pour optimiser les performances.");
}
// =========================
// MÉTHODES PRODUCTS (existantes)
// =========================
Future<int> createProduct(Product product) async {
final db = await database;
return await db.insert('products', product.toMap());
}
Future<List<Product>> getProducts() async {
final db = await database;
final maps = await db.query('products', orderBy: 'name ASC');
return List.generate(maps.length, (i) {
return Product.fromMap(maps[i]);
});
}
Future<int> updateProduct(Product product) async {
final db = await database;
return await db.update(
'products',
product.toMap(),
where: 'id = ?',
whereArgs: [product.id],
);
}
Future<int> deleteProduct(int? id) async {
final db = await database;
return await db.delete(
'products',
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<String>> getCategories() async {
final db = await database;
final result = await db.rawQuery('SELECT DISTINCT category FROM products ORDER BY category');
return List.generate(
result.length, (index) => result[index]['category'] as String);
}
Future<List<Product>> getProductsByCategory(String category) async {
final db = await database;
final maps = await db
.query('products', where: 'category = ?', whereArgs: [category], orderBy: 'name ASC');
return List.generate(maps.length, (i) {
return Product.fromMap(maps[i]);
});
}
Future<int> updateStock(int id, int stock) async {
final db = await database;
return await db
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
}
Future<Product?> getProductByReference(String reference) async {
final db = await database;
final maps = await db.query(
'products',
where: 'reference = ?',
whereArgs: [reference],
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
return null;
}
// =========================
// MÉTHODES CLIENTS
// =========================
Future<int> createClient(Client client) async {
final db = await database;
return await db.insert('clients', client.toMap());
}
Future<List<Client>> getClients() async {
final db = await database;
final maps = await db.query('clients', where: 'actif = 1', orderBy: 'nom ASC, prenom ASC');
return List.generate(maps.length, (i) {
return Client.fromMap(maps[i]);
});
}
Future<Client?> getClientById(int id) async {
final db = await database;
final maps = await db.query('clients', where: 'id = ?', whereArgs: [id]);
if (maps.isNotEmpty) {
return Client.fromMap(maps.first);
}
return null;
}
Future<int> updateClient(Client client) async {
final db = await database;
return await db.update(
'clients',
client.toMap(),
where: 'id = ?',
whereArgs: [client.id],
);
}
Future<int> deleteClient(int id) async {
final db = await database;
// Soft delete
return await db.update(
'clients',
{'actif': 0},
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<Client>> searchClients(String query) async {
final db = await database;
final maps = await db.query(
'clients',
where: 'actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?)',
whereArgs: ['%$query%', '%$query%', '%$query%'],
orderBy: 'nom ASC, prenom ASC',
);
return List.generate(maps.length, (i) {
return Client.fromMap(maps[i]);
});
}
// =========================
// MÉTHODES COMMANDES
// =========================
Future<int> createCommande(Commande commande) async {
final db = await database;
return await db.insert('commandes', commande.toMap());
}
Future<List<Commande>> getCommandes() async {
final db = await database;
final maps = await db.rawQuery('''
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
ORDER BY c.dateCommande DESC
''');
return List.generate(maps.length, (i) {
return Commande.fromMap(maps[i]);
});
}
Future<List<Commande>> getCommandesByClient(int clientId) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
WHERE c.clientId = ?
ORDER BY c.dateCommande DESC
''', [clientId]);
return List.generate(maps.length, (i) {
return Commande.fromMap(maps[i]);
});
}
Future<Commande?> getCommandeById(int id) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
WHERE c.id = ?
''', [id]);
if (maps.isNotEmpty) {
return Commande.fromMap(maps.first);
}
return null;
}
Future<int> updateCommande(Commande commande) async {
final db = await database;
return await db.update(
'commandes',
commande.toMap(),
where: 'id = ?',
whereArgs: [commande.id],
);
}
Future<int> updateStatutCommande(int commandeId, StatutCommande statut) async {
final db = await database;
return await db.update(
'commandes',
{'statut': statut.index},
where: 'id = ?',
whereArgs: [commandeId],
);
}
// =========================
// MÉTHODES DÉTAILS COMMANDES
// =========================
Future<int> createDetailCommande(DetailCommande detail) async {
final db = await database;
return await db.insert('details_commandes', detail.toMap());
}
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference
FROM details_commandes dc
LEFT JOIN products p ON dc.produitId = p.id
WHERE dc.commandeId = ?
ORDER BY dc.id
''', [commandeId]);
return List.generate(maps.length, (i) {
return DetailCommande.fromMap(maps[i]);
});
}
// =========================
// MÉTHODES TRANSACTION COMPLÈTE
// =========================
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async {
final db = await database;
return await db.transaction((txn) async {
// Créer le client
final clientId = await txn.insert('clients', client.toMap());
// Créer la commande
final commandeMap = commande.toMap();
commandeMap['clientId'] = clientId;
final commandeId = await txn.insert('commandes', commandeMap);
// Créer les détails et mettre à jour le stock
for (var detail in details) {
final detailMap = detail.toMap();
detailMap['commandeId'] = commandeId; // Ajoute l'ID de la commande
await txn.insert('details_commandes', detailMap);
// Mettre à jour le stock du produit
await txn.rawUpdate(
'UPDATE products SET stock = stock - ? WHERE id = ?',
[detail.quantite, detail.produitId],
);
}
return commandeId;
});
}
// =========================
// STATISTIQUES
// =========================
Future<Map<String, dynamic>> getStatistiques() async {
final db = await database;
final totalClients = await db.rawQuery('SELECT COUNT(*) as count FROM clients WHERE actif = 1');
final totalCommandes = await db.rawQuery('SELECT COUNT(*) as count FROM commandes');
final totalProduits = await db.rawQuery('SELECT COUNT(*) as count FROM products');
final chiffreAffaires = await db.rawQuery('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); // 5 = annulée
return {
'totalClients': totalClients.first['count'],
'totalCommandes': totalCommandes.first['count'],
'totalProduits': totalProduits.first['count'],
'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0,
};
}
// =========================
// DONNÉES PAR DÉFAUT
// =========================
Future<void> _insertDefaultClients() async {
final db = await database;
final existingClients = await db.query('clients');
if (existingClients.isEmpty) {
final defaultClients = [
Client(
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@email.com',
telephone: '0123456789',
adresse: '123 Rue de la Paix, Paris',
dateCreation: DateTime.now(),
),
Client(
nom: 'Martin',
prenom: 'Marie',
email: 'marie.martin@email.com',
telephone: '0987654321',
adresse: '456 Avenue des Champs, Lyon',
dateCreation: DateTime.now(),
),
Client(
nom: 'Bernard',
prenom: 'Pierre',
email: 'pierre.bernard@email.com',
telephone: '0456789123',
adresse: '789 Boulevard Saint-Michel, Marseille',
dateCreation: DateTime.now(),
),
];
for (var client in defaultClients) {
await db.insert('clients', client.toMap());
}
print("Clients par défaut insérés");
}
}
Future<void> _insertDefaultCommandes() async {
final db = await database;
final existingCommandes = await db.query('commandes');
if (existingCommandes.isEmpty) {
// Récupérer quelques produits pour créer des commandes
final produits = await db.query('products', limit: 3);
final clients = await db.query('clients', limit: 3);
if (produits.isNotEmpty && clients.isNotEmpty) {
// Commande 1
final commande1Id = await db.insert('commandes', {
'clientId': clients[0]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
'statut': StatutCommande.livree.index,
'montantTotal': 150.0,
'notes': 'Commande urgente',
});
await db.insert('details_commandes', {
'commandeId': commande1Id,
'produitId': produits[0]['id'],
'quantite': 2,
'prixUnitaire': 75.0,
'sousTotal': 150.0,
});
// Commande 2
final commande2Id = await db.insert('commandes', {
'clientId': clients[1]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
'statut': StatutCommande.enPreparation.index,
'montantTotal': 225.0,
'notes': 'Livraison prévue demain',
});
if (produits.length > 1) {
await db.insert('details_commandes', {
'commandeId': commande2Id,
'produitId': produits[1]['id'],
'quantite': 3,
'prixUnitaire': 75.0,
'sousTotal': 225.0,
});
}
// Commande 3
final commande3Id = await db.insert('commandes', {
'clientId': clients[2]['id'],
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
'statut': StatutCommande.confirmee.index,
'montantTotal': 300.0,
'notes': 'Commande standard',
});
if (produits.length > 2) {
await db.insert('details_commandes', {
'commandeId': commande3Id,
'produitId': produits[2]['id'],
'quantite': 4,
'prixUnitaire': 75.0,
'sousTotal': 300.0,
});
}
print("Commandes par défaut insérées");
}
}
}
Future<void> close() async {
if (_database.isOpen) {
await _database.close();
}
}
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue
Future<void> deleteDatabaseFile() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'products2.db');
final file = File(path);
if (await file.exists()) {
await file.delete();
print("Base de données product supprimée");
}
}
}

1090
lib/Services/qrService.dart

File diff suppressed because it is too large

3565
lib/Services/stock_managementDatabase.dart

File diff suppressed because it is too large

2524
lib/Views/Dashboard.dart

File diff suppressed because it is too large

847
lib/Views/DemandeTransfert.dart

@ -0,0 +1,847 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
class GestionTransfertsPage extends StatefulWidget {
const GestionTransfertsPage({super.key});
@override
_GestionTransfertsPageState createState() => _GestionTransfertsPageState();
}
class _GestionTransfertsPageState extends State<GestionTransfertsPage> with TickerProviderStateMixin {
final AppDatabase _appDatabase = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
List<Map<String, dynamic>> _demandes = [];
List<Map<String, dynamic>> _filteredDemandes = [];
bool _isLoading = false;
String _selectedStatut = 'en_attente';
String _searchQuery = '';
late TabController _tabController;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadDemandes();
_searchController.addListener(_filterDemandes);
}
@override
void dispose() {
_tabController.dispose();
_searchController.dispose();
super.dispose();
}
Future<void> _loadDemandes() async {
setState(() => _isLoading = true);
try {
List<Map<String, dynamic>> demandes;
switch (_selectedStatut) {
case 'en_attente':
demandes = await _appDatabase.getDemandesTransfertEnAttente();
break;
case 'validees':
demandes = await _appDatabase.getDemandesTransfertValidees();
break;
case 'toutes':
demandes = await _appDatabase.getToutesDemandesTransfert();
break;
default:
demandes = await _appDatabase.getDemandesTransfertEnAttente();
}
setState(() {
_demandes = demandes;
_filteredDemandes = demandes;
});
_filterDemandes();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de charger les demandes: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
setState(() => _isLoading = false);
}
}
void _filterDemandes() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredDemandes = _demandes.where((demande) {
final produitNom = (demande['produit_nom'] ?? '').toString().toLowerCase();
final produitRef = (demande['produit_reference'] ?? '').toString().toLowerCase();
final demandeurNom = (demande['demandeur_nom'] ?? '').toString().toLowerCase();
final pointVenteSource = (demande['point_vente_source'] ?? '').toString().toLowerCase();
final pointVenteDestination = (demande['point_vente_destination'] ?? '').toString().toLowerCase();
return produitNom.contains(query) ||
produitRef.contains(query) ||
demandeurNom.contains(query) ||
pointVenteSource.contains(query) ||
pointVenteDestination.contains(query);
}).toList();
});
}
Future<void> _validerDemande(int demandeId, Map<String, dynamic> demande) async {
// Vérifier seulement si le produit est en rupture de stock (stock = 0)
final stockDisponible = demande['stock_source'] as int? ?? 0;
if (stockDisponible == 0) {
await _showRuptureStockDialog(demande);
return;
}
final confirmation = await _showConfirmationDialog(demande);
if (!confirmation) return;
setState(() => _isLoading = true);
try {
await _appDatabase.validerTransfert(demandeId, _userController.userId);
Get.snackbar(
'Succès',
'Transfert validé avec succès',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
icon: const Icon(Icons.check_circle, color: Colors.white),
);
await _loadDemandes();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de valider le transfert: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
icon: const Icon(Icons.error, color: Colors.white),
);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _rejeterDemande(int demandeId, Map<String, dynamic> demande) async {
final motif = await _showRejectionDialog();
if (motif == null) return;
setState(() => _isLoading = true);
try {
await _appDatabase.rejeterTransfert(demandeId, _userController.userId, motif);
Get.snackbar(
'Demande rejetée',
'La demande de transfert a été rejetée',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
await _loadDemandes();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de rejeter la demande: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
setState(() => _isLoading = false);
}
}
Future<bool> _showConfirmationDialog(Map<String, dynamic> demande) async {
final stockDisponible = demande['stock_source'] as int? ?? 0;
final quantiteDemandee = demande['quantite'] as int;
final stockInsuffisant = stockDisponible < quantiteDemandee && stockDisponible > 0;
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.swap_horiz, color: Colors.blue.shade700),
const SizedBox(width: 8),
const Text('Confirmer le transfert'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Êtes-vous sûr de vouloir valider ce transfert ?'),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Produit: ${demande['produit_nom']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text('Référence: ${demande['produit_reference']}'),
Text('Quantité: ${demande['quantite']}'),
Text(demande['point_vente_source'] == demande['point_vente_destination']?'De: Non specifier' : 'De: ${demande['point_vente_source']}'),
Text('Vers: ${demande['point_vente_destination']}'),
Text(
'Stock disponible: $stockDisponible',
style: TextStyle(
color: stockInsuffisant ? Colors.orange.shade700 : Colors.green.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
),
if (stockInsuffisant) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: Row(
children: [
Icon(Icons.info, size: 16, color: Colors.orange.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'Le stock sera insuffisant après ce transfert',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('Valider'),
),
],
),
) ?? false;
}
Future<void> _showRuptureStockDialog(Map<String, dynamic> demande) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.warning, color: Colors.red.shade700),
const SizedBox(width: 8),
const Text('Rupture de stock'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Impossible d\'effectuer ce transfert car le produit est en rupture de stock.',
style: TextStyle(color: Colors.red.shade700),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Produit: ${demande['produit_nom']}'),
Text('Quantité demandée: ${demande['quantite']}'),
Text(
'Stock disponible: 0',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Compris'),
),
],
),
);
}
Future<String?> _showRejectionDialog() async {
final TextEditingController motifController = TextEditingController();
return await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Rejeter la demande'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Veuillez indiquer le motif du rejet :'),
const SizedBox(height: 12),
TextField(
controller: motifController,
decoration: const InputDecoration(
hintText: 'Motif du rejet',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (motifController.text.trim().isNotEmpty) {
Navigator.pop(context, motifController.text.trim());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
child: const Text('Rejeter'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
appBar: AppBar(
title: const Text('Gestion des transferts'),
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadDemandes,
tooltip: 'Actualiser',
),
],
bottom: TabBar(
controller: _tabController,
onTap: (index) {
setState(() {
switch (index) {
case 0:
_selectedStatut = 'en_attente';
break;
case 1:
_selectedStatut = 'validees';
break;
case 2:
_selectedStatut = 'toutes';
break;
}
});
_loadDemandes();
},
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
tabs: const [
Tab(
icon: Icon(Icons.pending_actions),
text: 'En attente',
),
Tab(
icon: Icon(Icons.check_circle),
text: 'Validées',
),
Tab(
icon: Icon(Icons.list),
text: 'Toutes',
),
],
),
),
drawer: CustomDrawer(),
body: Column(
children: [
// Barre de recherche
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par produit, référence, demandeur...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_filterDemandes();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
),
),
),
// Compteur de résultats
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Text(
'${_filteredDemandes.length} demande(s)',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const Spacer(),
if (_selectedStatut == 'en_attente' && _filteredDemandes.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Action requise',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
// Liste des demandes
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredDemandes.isEmpty
? _buildEmptyState()
: TabBarView(
controller: _tabController,
children: [
_buildDemandesEnAttente(),
_buildDemandesValidees(),
_buildToutesLesDemandes(),
],
),
),
],
),
);
}
Widget _buildEmptyState() {
String message;
IconData icon;
switch (_selectedStatut) {
case 'en_attente':
message = 'Aucune demande en attente';
icon = Icons.inbox;
break;
case 'validees':
message = 'Aucune demande validée';
icon = Icons.check_circle_outline;
break;
default:
message = 'Aucune demande trouvée';
icon = Icons.search_off;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
if (_searchController.text.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Aucun résultat pour "${_searchController.text}"',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
],
),
);
}
Widget _buildDemandesEnAttente() {
return _buildDemandesList(showActions: true);
}
Widget _buildDemandesValidees() {
return _buildDemandesList(showActions: false);
}
Widget _buildToutesLesDemandes() {
return _buildDemandesList(showActions: _selectedStatut == 'en_attente');
}
Widget _buildDemandesList({required bool showActions}) {
return RefreshIndicator(
onRefresh: _loadDemandes,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _filteredDemandes.length,
itemBuilder: (context, index) {
final demande = _filteredDemandes[index];
return _buildDemandeCard(demande, showActions);
},
),
);
}
Widget _buildDemandeCard(Map<String, dynamic> demande, bool showActions) {
final isMobile = MediaQuery.of(context).size.width < 600;
final statut = demande['statut'] as String? ?? 'en_attente';
final stockDisponible = demande['stock_source'] as int? ?? 0;
final quantiteDemandee = demande['quantite'] as int;
final enRuptureStock = stockDisponible == 0;
Color statutColor;
IconData statutIcon;
String statutText;
switch (statut) {
case 'validee':
statutColor = Colors.green;
statutIcon = Icons.check_circle;
statutText = 'Validée';
break;
case 'refusee':
statutColor = Colors.red;
statutIcon = Icons.cancel;
statutText = 'Rejetée';
break;
default:
statutColor = Colors.orange;
statutIcon = Icons.pending;
statutText = 'En attente';
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: enRuptureStock && statut == 'en_attente'
? BorderSide(color: Colors.red.shade300, width: 1.5)
: BorderSide.none,
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec produit et statut
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
demande['produit_nom'] ?? 'Produit inconnu',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'Réf: ${demande['produit_reference'] ?? 'N/A'}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statutColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statutColor.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statutIcon, size: 16, color: statutColor),
const SizedBox(width: 4),
Text(
statutText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: statutColor,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Informations de transfert
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Row(
children: [
Icon(Icons.store, size: 16, color: Colors.blue.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
demande['point_vente_source']==demande['point_vente_destination']?"Non specifier" : '${demande['point_vente_source'] ?? 'N/A'}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Icon(Icons.arrow_forward, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'${demande['point_vente_destination'] ?? 'N/A'}',
style: const TextStyle(fontWeight: FontWeight.w500),
textAlign: TextAlign.end,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.inventory_2, size: 16, color: Colors.green.shade600),
const SizedBox(width: 8),
Text('Quantité: $quantiteDemandee'),
const Spacer(),
Text(
'Stock source: $stockDisponible',
style: TextStyle(
color: enRuptureStock
? Colors.red.shade600
: stockDisponible < quantiteDemandee
? Colors.orange.shade600
: Colors.green.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
const SizedBox(height: 12),
// Informations de la demande
Row(
children: [
Icon(Icons.person, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'Demandé par: ${demande['demandeur_nom'] ?? 'N/A'}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.access_time, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text(
DateFormat('dd/MM/yyyy à HH:mm').format(
(demande['date_demande'] as DateTime).toLocal()
),
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
],
),
// Alerte rupture de stock
if (enRuptureStock && statut == 'en_attente') ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.warning, size: 16, color: Colors.red.shade600),
const SizedBox(width: 8),
Expanded(
child: Text(
'Produit en rupture de stock',
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
// Actions (seulement pour les demandes en attente)
if (showActions && statut == 'en_attente') ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _rejeterDemande(
demande['id'] as int,
demande,
),
icon: const Icon(Icons.close, size: 18),
label: Text(isMobile ? 'Rejeter' : 'Rejeter'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red.shade600,
side: BorderSide(color: Colors.red.shade300),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: !enRuptureStock
? () => _validerDemande(
demande['id'] as int,
demande,
)
: null,
icon: const Icon(Icons.check, size: 18),
label: Text(isMobile ? 'Valider' : 'Valider'),
style: ElevatedButton.styleFrom(
backgroundColor: !enRuptureStock
? Colors.green.shade600
: Colors.grey.shade400,
foregroundColor: Colors.white,
),
),
),
],
),
],
],
),
),
);
}
}

5662
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

7
lib/Views/RoleListPage.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart';
//import 'package:youmazgestion/Models/Permission.dart';
//import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/RolePermissionPage.dart';
class RoleListPage extends StatefulWidget {
@ -47,7 +48,7 @@ class _RoleListPageState extends State<RoleListPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: "Gestion des rôles"),
appBar: CustomAppBar(title: "Gestion des rôles"),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(

610
lib/Views/RolePermissionPage.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class RolePermissionsPage extends StatefulWidget {
final Role role;
@ -18,6 +19,8 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
List<Permission> permissions = [];
List<Map<String, dynamic>> menus = [];
Map<int, Map<String, bool>> menuPermissionsMap = {};
bool isLoading = true;
String? errorMessage;
@override
void initState() {
@ -26,152 +29,559 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
}
Future<void> _initData() async {
final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
try {
setState(() {
isLoading = true;
errorMessage = null;
});
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
final perms = await db.getAllPermissions();
final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(
widget.role.id!, menuId);
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
tempMenuPermissionsMap[menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
}
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(
widget.role.id!, menuId);
setState(() {
permissions = perms;
menus = menuList;
menuPermissionsMap = tempMenuPermissionsMap;
});
tempMenuPermissionsMap[menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
}
setState(() {
permissions = perms;
menus = menuList;
menuPermissionsMap = tempMenuPermissionsMap;
isLoading = false;
});
} catch (e) {
setState(() {
errorMessage = 'Erreur lors du chargement des données: $e';
isLoading = false;
});
print("Erreur lors de l'initialisation des données: $e");
}
}
Future<void> _onPermissionToggle(
int menuId, String permission, bool enabled) async {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
await db.assignRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
try {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
await db.assignRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(
widget.role.id!, menuId, perm.id!);
}
setState(() {
menuPermissionsMap[menuId]![permission] = enabled;
});
// Afficher un message de confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
enabled
? 'Permission "$permission" accordée'
: 'Permission "$permission" révoquée',
),
backgroundColor: enabled ? Colors.green : Colors.orange,
duration: const Duration(seconds: 2),
),
);
} catch (e) {
print("Erreur lors de la modification de la permission: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la modification: $e'),
backgroundColor: Colors.red,
),
);
}
}
setState(() {
menuPermissionsMap[menuId]![permission] = enabled;
});
void _toggleAllPermissions(int menuId, bool enabled) {
for (var permission in permissions) {
_onPermissionToggle(menuId, permission.name, enabled);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}",
// showBackButton: true,
),
body: Padding(
int _getSelectedPermissionsCount(int menuId) {
return menuPermissionsMap[menuId]?.values.where((selected) => selected).length ?? 0;
}
double _getPermissionPercentage(int menuId) {
if (permissions.isEmpty) return 0.0;
return _getSelectedPermissionsCount(menuId) / permissions.length;
}
Widget _buildPermissionSummary() {
int totalPermissions = menus.length * permissions.length;
int selectedPermissions = 0;
for (var menuId in menuPermissionsMap.keys) {
selectedPermissions += _getSelectedPermissionsCount(menuId);
}
double percentage = totalPermissions > 0 ? selectedPermissions / totalPermissions : 0.0;
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestion des permissions pour le rôle: ${widget.role.designation}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
Row(
children: [
Icon(Icons.analytics, color: Colors.blue.shade600),
const SizedBox(width: 8),
Text(
'Résumé des permissions',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
],
),
const SizedBox(height: 10),
const Text(
'Sélectionnez les permissions pour chaque menu:',
style: TextStyle(fontSize: 14, color: Colors.grey),
const SizedBox(height: 12),
LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
percentage > 0.7 ? Colors.green :
percentage > 0.3 ? Colors.orange : Colors.red,
),
),
const SizedBox(height: 20),
if (permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: menus.length,
itemBuilder: (context, index) {
final menu = menus[index];
final menuId = menu['id'] as int;
final menuName = menu['name'] as String;
return Card(
margin: const EdgeInsets.only(bottom: 15),
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menuName,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Wrap(
spacing: 10,
runSpacing: 10,
children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return FilterChip(
label: perm.name,
selected: isChecked,
onSelected: (bool value) {
_onPermissionToggle(menuId, perm.name, value);
},
);
}).toList(),
),
],
),
),
);
},
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$selectedPermissions / $totalPermissions permissions',
style: const TextStyle(fontWeight: FontWeight.w500),
),
Text(
'${(percentage * 100).toStringAsFixed(1)}%',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
],
),
),
);
}
Widget _buildMenuCard(Map<String, dynamic> menu) {
final menuId = menu['id'] as int;
final menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
final selectedCount = _getSelectedPermissionsCount(menuId);
final percentage = _getPermissionPercentage(menuId);
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ExpansionTile(
leading: CircleAvatar(
backgroundColor: percentage == 1.0 ? Colors.green :
percentage > 0 ? Colors.orange : Colors.red.shade100,
child: Icon(
Icons.menu,
color: percentage == 1.0 ? Colors.white :
percentage > 0 ? Colors.white : Colors.red,
),
),
title: Text(
menuName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
percentage == 1.0 ? Colors.green :
percentage > 0 ? Colors.orange : Colors.red,
),
),
),
)
else
const Expanded(
child: Center(
child: CircularProgressIndicator(),
const SizedBox(width: 8),
Text(
'$selectedCount/${permissions.length}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
trailing: PopupMenuButton<String>(
onSelected: (value) {
if (value == 'all') {
_toggleAllPermissions(menuId, true);
} else if (value == 'none') {
_toggleAllPermissions(menuId, false);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'all',
child: Row(
children: [
Icon(Icons.select_all, color: Colors.green),
SizedBox(width: 8),
Text('Tout sélectionner'),
],
),
),
const PopupMenuItem(
value: 'none',
child: Row(
children: [
Icon(Icons.deselect, color: Colors.red),
SizedBox(width: 8),
Text('Tout désélectionner'),
],
),
),
],
child: const Icon(Icons.more_vert),
),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Permissions disponibles:',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return CustomFilterChip(
label: perm.name,
selected: isChecked,
onSelected: (bool value) {
_onPermissionToggle(menuId, perm.name, value);
},
);
}).toList(),
),
],
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}",
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red.shade600,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
errorMessage!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
],
),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec informations du rôle
Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
CircleAvatar(
backgroundColor: widget.role.designation == 'Super Admin'
? Colors.red.shade100
: Colors.blue.shade100,
radius: 24,
child: Icon(
Icons.person,
color: widget.role.designation == 'Super Admin'
? Colors.red.shade700
: Colors.blue.shade700,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestion des permissions',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Rôle: ${widget.role.designation}',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Configurez les accès pour chaque menu',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
),
// Résumé des permissions
if (permissions.isNotEmpty && menus.isNotEmpty)
_buildPermissionSummary(),
// Liste des menus et permissions
if (permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: menus.length,
itemBuilder: (context, index) {
return _buildMenuCard(menus[index]);
},
),
)
else
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucune donnée disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Permissions: ${permissions.length} | Menus: ${menus.length}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Actualiser'),
),
],
),
),
),
],
),
),
floatingActionButton: !isLoading && errorMessage == null
? FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.save),
label: const Text('Enregistrer'),
backgroundColor: Colors.green,
)
: null,
);
}
}
class FilterChip extends StatelessWidget {
class CustomFilterChip extends StatelessWidget {
final String label;
final bool selected;
final ValueChanged<bool> onSelected;
const FilterChip({
const CustomFilterChip({
super.key,
required this.label,
required this.selected,
required this.onSelected,
});
Color _getChipColor(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Colors.blue;
case 'create':
return Colors.green;
case 'update':
return Colors.orange;
case 'delete':
return Colors.red;
case 'admin':
return Colors.purple;
case 'manage':
return Colors.indigo;
default:
return Colors.grey;
}
}
IconData _getChipIcon(String label) {
switch (label.toLowerCase()) {
case 'view':
case 'read':
return Icons.visibility;
case 'create':
return Icons.add;
case 'update':
return Icons.edit;
case 'delete':
return Icons.delete;
case 'admin':
return Icons.admin_panel_settings;
case 'manage':
return Icons.settings;
default:
return Icons.security;
}
}
@override
Widget build(BuildContext context) {
return ChoiceChip(
label: Text(label),
final color = _getChipColor(label);
final icon = _getChipIcon(label);
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: selected ? Colors.white : color,
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: selected ? Colors.white : color,
fontWeight: FontWeight.w500,
),
),
],
),
selected: selected,
onSelected: onSelected,
selectedColor: Colors.blue,
labelStyle: TextStyle(
color: selected ? Colors.white : Colors.black,
),
selectedColor: color,
backgroundColor: color.withOpacity(0.1),
checkmarkColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: selected ? color : color.withOpacity(0.3),
width: 1,
),
),
elevation: selected ? 4 : 1,
pressElevation: 8,
);
}
}

452
lib/Views/approbation_sorties_page.dart

@ -0,0 +1,452 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
class ApprobationSortiesPage extends StatefulWidget {
const ApprobationSortiesPage({super.key});
@override
_ApprobationSortiesPageState createState() => _ApprobationSortiesPageState();
}
class _ApprobationSortiesPageState extends State<ApprobationSortiesPage> {
final AppDatabase _database = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
List<Map<String, dynamic>> _sortiesEnAttente = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadSortiesEnAttente();
}
Future<void> _loadSortiesEnAttente() async {
setState(() => _isLoading = true);
try {
final sorties = await _database.getSortiesPersonnellesEnAttente();
setState(() {
_sortiesEnAttente = sorties;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar('Erreur', 'Impossible de charger les demandes: $e');
}
}
Future<void> _approuverSortie(Map<String, dynamic> sortie) async {
final confirm = await Get.dialog<bool>(
AlertDialog(
title: const Text('Approuver la demande'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Produit: ${sortie['produit_nom']}'),
Text('Quantité: ${sortie['quantite']}'),
Text('Demandeur: ${sortie['admin_nom']}'),
Text('Motif: ${sortie['motif']}'),
Text('Note: ${sortie['notes']}'),
const SizedBox(height: 16),
const Text(
'Confirmer l\'approbation de cette demande de sortie personnelle ?',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
child: const Text('Approuver', style: TextStyle(color: Colors.white)),
),
],
),
);
if (confirm == true) {
try {
await _database.approuverSortiePersonnelle(
sortie['id'] as int,
_userController.userId,
);
Get.snackbar(
'Demande approuvée',
'La sortie personnelle a été approuvée et le stock mis à jour',
backgroundColor: Colors.green,
colorText: Colors.white,
);
_loadSortiesEnAttente();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible d\'approuver la demande: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
Future<void> _refuserSortie(Map<String, dynamic> sortie) async {
final motifController = TextEditingController();
final confirm = await Get.dialog<bool>(
AlertDialog(
title: const Text('Refuser la demande'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Demande de: ${sortie['admin_nom']}'),
Text('Produit: ${sortie['produit_nom']}'),
const SizedBox(height: 16),
TextField(
controller: motifController,
decoration: const InputDecoration(
labelText: 'Motif du refus *',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (motifController.text.trim().isNotEmpty) {
Get.back(result: true);
} else {
Get.snackbar('Erreur', 'Veuillez indiquer un motif de refus');
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Refuser', style: TextStyle(color: Colors.white)),
),
],
),
);
if (confirm == true && motifController.text.trim().isNotEmpty) {
try {
await _database.refuserSortiePersonnelle(
sortie['id'] as int,
_userController.userId,
motifController.text.trim(),
);
Get.snackbar(
'Demande refusée',
'La sortie personnelle a été refusée',
backgroundColor: Colors.orange,
colorText: Colors.white,
);
_loadSortiesEnAttente();
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de refuser la demande: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Approbation sorties personnelles'),
drawer: CustomDrawer(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadSortiesEnAttente,
child: _sortiesEnAttente.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucune demande en attente',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _sortiesEnAttente.length,
itemBuilder: (context, index) {
final sortie = _sortiesEnAttente[index];
return _buildSortieCard(sortie);
},
),
),
);
}
Widget _buildSortieCard(Map<String, dynamic> sortie) {
final dateSortie = DateTime.parse(sortie['date_sortie'].toString());
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec statut
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'EN ATTENTE',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
),
const Spacer(),
Text(
DateFormat('dd/MM/yyyy HH:mm').format(dateSortie),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 12),
// Informations du produit
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.inventory, color: Colors.blue.shade700, size: 16),
const SizedBox(width: 8),
Text(
'Produit demandé',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
],
),
const SizedBox(height: 8),
Text(
sortie['produit_nom'].toString(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Row(
children: [
Text('Référence: ${sortie['produit_reference'] ?? 'N/A'}'),
const SizedBox(width: 16),
Text('Stock actuel: ${sortie['stock_actuel']}'),
const SizedBox(width: 16),
Text(
'Quantité demandée: ${sortie['quantite']}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
],
),
),
const SizedBox(height: 12),
// Informations du demandeur
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person, color: Colors.green.shade700, size: 16),
const SizedBox(width: 8),
Text(
'Demandeur',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
),
],
),
const SizedBox(height: 8),
Text(
'${sortie['admin_nom']} ${sortie['admin_nom_famille'] ?? ''}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (sortie['point_vente_nom'] != null)
Text('Point de vente: ${sortie['point_vente_nom']}'),
],
),
),
const SizedBox(height: 12),
// Motif
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.note, color: Colors.purple.shade700, size: 16),
const SizedBox(width: 8),
Text(
'Motif',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.purple.shade700,
),
),
],
),
const SizedBox(height: 8),
Text(
sortie['motif'].toString(),
style: const TextStyle(fontSize: 14),
),
if (sortie['notes'] != null && sortie['notes'].toString().isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Notes: ${sortie['notes']}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontStyle: FontStyle.italic,
),
),
],
],
),
),
const SizedBox(height: 16),
// Vérification de stock
if ((sortie['stock_actuel'] as int) < (sortie['quantite'] as int))
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade300),
),
child: Row(
children: [
Icon(Icons.warning, color: Colors.red.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
'ATTENTION: Stock insuffisant pour cette demande',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 16),
// Boutons d'action
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _refuserSortie(sortie),
icon: const Icon(Icons.close, size: 18),
label: const Text('Refuser'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: ((sortie['stock_actuel'] as int) >= (sortie['quantite'] as int))
? () => _approuverSortie(sortie)
: null,
icon: const Icon(Icons.check, size: 18),
label: const Text('Approuver'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
],
),
),
);
}
}

5
lib/Views/bilanMois.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/controller/HistoryController.dart';
import 'package:intl/intl.dart';
import 'bilanDesJourne.dart';
@ -29,7 +30,7 @@ class _BilanMoisState extends State<BilanMois> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: 'Bilan du mois'),
appBar: CustomAppBar(title: 'Bilan du mois'),
body: Column(
children: [
// Les 3 cartes en haut
@ -38,7 +39,7 @@ class _BilanMoisState extends State<BilanMois> {
children: [
_buildInfoCard(
title: 'Chiffre réalisé',
value: '${controller.totalSum.value.toStringAsFixed(2)} MGA',
value: '${NumberFormat('#,##0', 'fr_FR').format(controller.totalSum.value)} MGA',
color: Colors.green,
icon: Icons.monetization_on,
),

4001
lib/Views/commandManagement.dart

File diff suppressed because it is too large

979
lib/Views/demande_sortie_personnelle_page.dart

@ -0,0 +1,979 @@
// Importations nécessaires
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image/image.dart' as img;
import 'package:zxing2/qrcode.dart';
import 'package:camera/camera.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
import '../Models/produit.dart';
class DemandeSortiePersonnellePage extends StatefulWidget {
const DemandeSortiePersonnellePage({super.key});
@override
_DemandeSortiePersonnellePageState createState() =>
_DemandeSortiePersonnellePageState();
}
class _DemandeSortiePersonnellePageState
extends State<DemandeSortiePersonnellePage> with TickerProviderStateMixin {
final AppDatabase _database = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
final _formKey = GlobalKey<FormState>();
final _quantiteController = TextEditingController(text: '1');
final _motifController = TextEditingController();
final _notesController = TextEditingController();
final _searchController = TextEditingController();
Product? _selectedProduct;
List<Product> _products = [];
List<Product> _filteredProducts = [];
bool _isLoading = false;
bool _isSearching = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
);
_loadProducts();
_searchController.addListener(_filterProducts);
}
/// ----------------- SCAN QR CODE -----------------
CameraController? _cameraController;
bool _isScanning = false;
Future<void> _scanQrOrBarcode() async {
if (defaultTargetPlatform == TargetPlatform.windows) {
final cameras = await availableCameras();
if (cameras.isEmpty) {
_showErrorSnackbar("Aucune caméra détectée");
return;
}
// Disposer l'ancien contrôleur
await _cameraController?.dispose();
_cameraController = CameraController(
cameras.first,
ResolutionPreset.high, // Meilleure résolution pour QR
enableAudio: false,
);
try {
await _cameraController!.initialize();
} catch (e) {
_showErrorSnackbar("Erreur initialisation caméra: $e");
return;
}
_isScanning = true;
Future<void> scanLoop() async {
// Attendre que le dialog soit affiché
await Future.delayed(const Duration(milliseconds: 300));
while (_isScanning && _cameraController != null) {
try {
final XFile file = await _cameraController!.takePicture();
final bytes = await file.readAsBytes();
// Décoder l'image
final imageDecoded = img.decodeImage(bytes);
if (imageDecoded == null) {
print("❌ Image non décodée");
await Future.delayed(const Duration(milliseconds: 800));
continue;
}
print("✅ Image décodée: ${imageDecoded.width}x${imageDecoded.height}");
// CORRECTION CRITIQUE: Convertir en format RGB correct
final rgbBytes = <int>[];
for (int y = 0; y < imageDecoded.height; y++) {
for (int x = 0; x < imageDecoded.width; x++) {
final pixel = imageDecoded.getPixel(x, y);
rgbBytes.add(pixel.r.toInt());
rgbBytes.add(pixel.g.toInt());
rgbBytes.add(pixel.b.toInt());
}
}
// Créer Int32List pour ZXing
final int32Data = Int32List(imageDecoded.width * imageDecoded.height);
for (int i = 0; i < imageDecoded.height; i++) {
for (int j = 0; j < imageDecoded.width; j++) {
final idx = i * imageDecoded.width + j;
final rgbIdx = idx * 3;
final r = rgbBytes[rgbIdx];
final g = rgbBytes[rgbIdx + 1];
final b = rgbBytes[rgbIdx + 2];
// Format ARGB
int32Data[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
}
}
final luminanceSource = RGBLuminanceSource(
imageDecoded.width,
imageDecoded.height,
int32Data,
);
final bitmap = BinaryBitmap(HybridBinarizer(luminanceSource));
final reader = QRCodeReader();
try {
final result = reader.decode(bitmap);
if (result.text.isNotEmpty) {
print("✅ QR Code détecté: ${result.text}");
_isScanning = false;
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
setState(() {
_searchController.text = result.text;
});
_filterProducts();
_showSuccessSnackbar("QR Code détecté : ${result.text}");
break;
}
} on NotFoundException catch (_) {
// Pas de QR trouvé dans cette frame
print("⚠️ Pas de QR trouvé");
} catch (e) {
print("❌ Erreur décodage QR: $e");
}
} catch (e) {
print("❌ Erreur capture: $e");
}
// Délai plus long pour éviter la surcharge
await Future.delayed(const Duration(milliseconds: 800));
}
}
// Lancer la boucle AVANT d'afficher le dialog
final scanFuture = scanLoop();
await showDialog(
context: context,
barrierDismissible: true,
builder: (context) {
return WillPopScope(
onWillPop: () async {
_isScanning = false;
return true;
},
child: AlertDialog(
title: const Text('Scanner le QR Code'),
content: SizedBox(
width: 640,
height: 480,
child: _cameraController != null &&
_cameraController!.value.isInitialized
? CameraPreview(_cameraController!)
: const Center(child: CircularProgressIndicator()),
),
actions: [
TextButton(
onPressed: () {
_isScanning = false;
Navigator.of(context).pop();
},
child: const Text('Annuler'),
),
],
),
);
},
);
_isScanning = false;
await scanFuture; // Attendre la fin de la boucle
await _cameraController?.dispose();
_cameraController = null;
return;
}
// 📱 Mobile et macOS mobile_scanner
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Scanner le QR Code'),
content: SizedBox(
width: double.maxFinite,
height: 400,
child: MobileScanner(
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isNotEmpty) {
final scanResult = barcodes.first.rawValue;
if (scanResult != null && scanResult.isNotEmpty) {
Navigator.of(context).pop();
setState(() => _searchController.text = scanResult);
_filterProducts();
_showSuccessSnackbar("QR Code détecté : $scanResult");
}
}
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
],
),
);
}
/// ----------------- FILTRAGE PRODUITS -----------------
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
if (query.isEmpty) {
_filteredProducts = _products;
_isSearching = false;
} else {
_isSearching = true;
_filteredProducts = _products.where((product) {
return product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false);
}).toList();
}
});
}
Future<void> _loadProducts() async {
setState(() => _isLoading = true);
try {
final products = await _database.getProducts();
setState(() {
_products = products.where((p) {
// Check stock availability
print("point de vente id: ${_userController.pointDeVenteId}");
bool hasStock = _userController.pointDeVenteId == 0
? (p.stock ?? 0) > 0
: (p.stock ?? 0) > 0 &&
p.pointDeVenteId == _userController.pointDeVenteId;
return hasStock;
}).toList();
// Setting filtered products
_filteredProducts = _products;
// End loading state
_isLoading = false;
});
// Start the animation
_animationController.forward();
} catch (e) {
// Handle any errors
setState(() {
_isLoading = false;
});
_showErrorSnackbar('Impossible de charger les produits: $e');
}
}
Future<void> _soumettreDemandePersonnelle() async {
if (!_formKey.currentState!.validate() || _selectedProduct == null) {
_showErrorSnackbar('Veuillez remplir tous les champs obligatoires');
return;
}
final quantite = int.tryParse(_quantiteController.text) ?? 0;
if (quantite <= 0) {
_showErrorSnackbar('La quantité doit être supérieure à 0');
return;
}
if ((_selectedProduct!.stock ?? 0) < quantite) {
_showErrorSnackbar(
'Stock insuffisant (disponible: ${_selectedProduct!.stock})');
return;
}
// Confirmation dialog
final confirmed = await _showConfirmationDialog();
if (!confirmed) return;
setState(() => _isLoading = true);
try {
await _database.createSortieStockPersonnelle(
produitId: _selectedProduct!.id!,
adminId: _userController.userId,
quantite: quantite,
motif: _motifController.text.trim(),
pointDeVenteId: _userController.pointDeVenteId > 0
? _userController.pointDeVenteId
: null,
notes: _notesController.text.trim().isNotEmpty
? _notesController.text.trim()
: null,
);
_showSuccessSnackbar(
'Votre demande de sortie personnelle a été soumise pour approbation');
// Réinitialiser le formulaire avec animation
_resetForm();
_loadProducts();
} catch (e) {
_showErrorSnackbar('Impossible de soumettre la demande: $e');
} finally {
setState(() => _isLoading = false);
}
}
void _resetForm() {
_formKey.currentState!.reset();
_quantiteController.text = '1';
_motifController.clear();
_notesController.clear();
_searchController.clear();
setState(() {
_selectedProduct = null;
_isSearching = false;
});
}
Future<bool> _showConfirmationDialog() async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.help_outline, color: Colors.orange.shade700),
const SizedBox(width: 8),
const Text('Confirmer la demande'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Êtes-vous sûr de vouloir soumettre cette demande ?'),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Produit: ${_selectedProduct?.name}'),
Text('Quantité: ${_quantiteController.text}'),
Text('Motif: ${_motifController.text}'),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade700,
foregroundColor: Colors.white,
),
child: const Text('Confirmer'),
),
],
),
) ??
false;
}
void _showSuccessSnackbar(String message) {
Get.snackbar(
'',
'',
titleText: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
const Text('Succès',
style:
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
messageText: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: Colors.green.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 4),
margin: const EdgeInsets.all(16),
borderRadius: 12,
icon: Icon(Icons.check_circle_outline, color: Colors.white),
);
}
void _showErrorSnackbar(String message) {
Get.snackbar(
'',
'',
titleText: Row(
children: [
Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
const Text('Erreur',
style:
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
messageText: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: Colors.red.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 4),
margin: const EdgeInsets.all(16),
borderRadius: 12,
);
}
Widget _buildHeaderCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade600, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.shade200,
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.inventory_2, color: Colors.white, size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sortie personnelle de stock',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'Demande d\'approbation requise',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Cette fonctionnalité permet aux administrateurs de demander '
'la sortie d\'un produit du stock pour usage personnel. '
'Toute demande nécessite une approbation avant traitement.',
style: TextStyle(fontSize: 14, color: Colors.white),
),
),
],
),
);
}
Widget _buildProductSelector() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sélection du produit *',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 12),
// Barre de recherche
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher un produit...',
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600),
),
onChanged: (value) {
_filterProducts(); // Call to filter products
},
),
),
IconButton(
icon: Icon(Icons.qr_code_scanner, color: Colors.blue),
onPressed: _scanQrOrBarcode,
tooltip: 'Scanner QR ou code-barres',
),
],
),
const SizedBox(height: 12),
// Liste des produits
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: _filteredProducts.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off,
size: 48, color: Colors.grey.shade400),
const SizedBox(height: 8),
Text(
_isSearching
? 'Aucun produit trouvé'
: 'Aucun produit disponible',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
)
: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
final isSelected = _selectedProduct?.id == product.id;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isSelected
? Colors.orange.shade50
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Colors.orange.shade300
: Colors.transparent,
width: 2,
),
),
child: ListTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isSelected
? Colors.orange.shade100
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.inventory,
color: isSelected
? Colors.orange.shade700
: Colors.grey.shade600,
),
),
title: Text(
product.name,
style: TextStyle(
fontWeight:
isSelected ? FontWeight.bold : FontWeight.w500,
color: isSelected
? Colors.orange.shade800
: Colors.grey.shade800,
),
),
subtitle: Text(
'Stock: ${product.stock} • Réf: ${product.reference ?? 'N/A'}',
style: TextStyle(
color: isSelected
? Colors.orange.shade600
: Colors.grey.shade600,
),
),
trailing: isSelected
? Icon(Icons.check_circle,
color: Colors.orange.shade700)
: Icon(Icons.radio_button_unchecked,
color: Colors.grey.shade400),
onTap: () {
setState(() {
_selectedProduct = product;
});
},
),
);
},
),
),
],
);
}
Widget _buildFormSection() {
return Column(
children: [
// Quantité
_buildInputField(
label: 'Quantité *',
controller: _quantiteController,
keyboardType: TextInputType.number,
icon: Icons.format_list_numbered,
suffix: _selectedProduct != null
? Text('max: ${_selectedProduct!.stock}',
style: TextStyle(color: Colors.grey.shade600))
: null,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer une quantité';
}
final quantite = int.tryParse(value);
if (quantite == null || quantite <= 0) {
return 'Quantité invalide';
}
if (_selectedProduct != null &&
quantite > (_selectedProduct!.stock ?? 0)) {
return 'Quantité supérieure au stock disponible';
}
return null;
},
),
const SizedBox(height: 20),
// Motif
_buildInputField(
label: 'Motif *',
controller: _motifController,
icon: Icons.description,
hintText: 'Raison de cette sortie personnelle',
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Veuillez indiquer le motif';
}
if (value.trim().length < 5) {
return 'Le motif doit contenir au moins 5 caractères';
}
return null;
},
),
const SizedBox(height: 20),
// Notes
_buildInputField(
label: 'Notes complémentaires',
controller: _notesController,
icon: Icons.note_add,
hintText: 'Informations complémentaires (optionnel)',
maxLines: 3,
),
],
);
}
Widget _buildInputField({
required String label,
required TextEditingController controller,
required IconData icon,
String? hintText,
TextInputType? keyboardType,
int maxLines = 1,
Widget? suffix,
String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
validator: validator,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: Icon(icon, color: Colors.grey.shade600),
suffix: suffix,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.orange.shade400, width: 2),
),
filled: true,
fillColor: Colors.grey.shade50,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
],
);
}
Widget _buildUserInfoCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person, color: Colors.grey.shade700),
const SizedBox(width: 8),
Text(
'Informations de la demande',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
],
),
const SizedBox(height: 12),
_buildInfoRow(
Icons.account_circle, 'Demandeur', _userController.name),
if (_userController.pointDeVenteId > 0)
_buildInfoRow(Icons.store, 'Point de vente',
_userController.pointDeVenteDesignation),
_buildInfoRow(Icons.calendar_today, 'Date',
DateTime.now().toLocal().toString().split(' ')[0]),
],
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text(
'$label: ',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
Expanded(
child: Text(
value,
style: TextStyle(color: Colors.grey.shade800),
),
),
],
),
);
}
Widget _buildSubmitButton() {
return Container(
width: double.infinity,
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.orange.shade700, Colors.orange.shade500],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.orange.shade300,
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: _isLoading ? null : _soumettreDemandePersonnelle,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: _isLoading
? const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
SizedBox(width: 12),
Text(
'Traitement...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.send, color: Colors.white),
SizedBox(width: 8),
Text(
'Soumettre la demande',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Demande sortie personnelle'),
drawer: CustomDrawer(),
body: _isLoading && _products.isEmpty
? const Center(child: CircularProgressIndicator())
: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderCard(),
const SizedBox(height: 24),
_buildProductSelector(),
const SizedBox(height: 24),
_buildFormSection(),
const SizedBox(height: 24),
_buildUserInfoCard(),
const SizedBox(height: 32),
_buildSubmitButton(),
const SizedBox(height: 16),
],
),
),
),
),
),
);
}
@override
void dispose() {
_animationController.dispose();
_quantiteController.dispose();
_motifController.dispose();
_notesController.dispose();
_searchController.dispose();
super.dispose();
}
}

5
lib/Views/editProduct.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Models/produit.dart';
import '../Services/productDatabase.dart';
//import '../Services/productDatabase.dart';
import 'gestionProduct.dart';
class EditProductPage extends StatelessWidget {
@ -31,7 +32,7 @@ class EditProductPage extends StatelessWidget {
category: category,
);
await ProductDatabase.instance.updateProduct(updatedProduct);
await AppDatabase.instance.updateProduct(updatedProduct);
Get.to(GestionProduit());
} else {

418
lib/Views/editUser.dart

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Models/role.dart';
import '../Services/app_database.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class EditUserPage extends StatefulWidget {
final Users user;
@ -20,9 +20,11 @@ class _EditUserPageState extends State<EditUserPage> {
late TextEditingController _passwordController;
List<Role> _roles = [];
List<Map<String, dynamic>> _pointsDeVente = [];
Role? _selectedRole;
Map<String, dynamic>? _selectedPointDeVente;
bool _isLoading = false;
bool _isLoadingRoles = true;
bool _isLoadingData = true;
@override
void initState() {
@ -33,28 +35,47 @@ class _EditUserPageState extends State<EditUserPage> {
_usernameController = TextEditingController(text: widget.user.username);
_passwordController = TextEditingController();
_loadRoles();
_loadInitialData();
}
Future<void> _loadRoles() async {
Future<void> _loadInitialData() async {
try {
// Charger les rôles
final roles = await AppDatabase.instance.getRoles();
final currentRole = roles.firstWhere(
(r) => r.id == widget.user.roleId,
orElse: () => Role(id: widget.user.roleId, designation: widget.user.roleName ?? 'Inconnu'),
);
// Charger les points de vente
final pointsDeVente = await AppDatabase.instance.getPointsDeVente();
// Trouver le point de vente actuel de l'utilisateur
Map<String, dynamic>? currentPointDeVente;
if (widget.user.pointDeVenteId != null) {
try {
currentPointDeVente = pointsDeVente.firstWhere(
(pv) => pv['id'] == widget.user.pointDeVenteId,
);
} catch (e) {
// Point de vente non trouvé, on garde null
print('Point de vente ${widget.user.pointDeVenteId} non trouvé');
}
}
setState(() {
_roles = roles;
_selectedRole = currentRole;
_isLoadingRoles = false;
_pointsDeVente = pointsDeVente;
_selectedPointDeVente = currentPointDeVente;
_isLoadingData = false;
});
} catch (e) {
print('Erreur lors du chargement des rôles: $e');
print('Erreur lors du chargement des données: $e');
setState(() {
_isLoadingRoles = false;
_isLoadingData = false;
});
_showErrorDialog('Erreur', 'Impossible de charger les rôles.');
_showErrorDialog('Erreur', 'Impossible de charger les données nécessaires.');
}
}
@ -68,13 +89,30 @@ class _EditUserPageState extends State<EditUserPage> {
super.dispose();
}
// AMÉLIORÉ: Validation des champs avec messages plus précis
bool _validateFields() {
if (_nameController.text.trim().isEmpty ||
_lastNameController.text.trim().isEmpty ||
_emailController.text.trim().isEmpty ||
_usernameController.text.trim().isEmpty ||
_selectedRole == null) {
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs requis.');
if (_nameController.text.trim().isEmpty) {
_showErrorDialog('Champ manquant', 'Le prénom est requis.');
return false;
}
if (_lastNameController.text.trim().isEmpty) {
_showErrorDialog('Champ manquant', 'Le nom de famille est requis.');
return false;
}
if (_emailController.text.trim().isEmpty) {
_showErrorDialog('Champ manquant', 'L\'email est requis.');
return false;
}
if (_usernameController.text.trim().isEmpty) {
_showErrorDialog('Champ manquant', 'Le nom d\'utilisateur est requis.');
return false;
}
if (_selectedRole == null) {
_showErrorDialog('Champ manquant', 'Veuillez sélectionner un rôle.');
return false;
}
@ -86,13 +124,19 @@ class _EditUserPageState extends State<EditUserPage> {
if (_passwordController.text.isNotEmpty &&
_passwordController.text.length < 6) {
_showErrorDialog('Mot de passe trop court', 'Minimum 6 caractères.');
_showErrorDialog('Mot de passe trop court', 'Le mot de passe doit contenir au minimum 6 caractères.');
return false;
}
if (_usernameController.text.trim().length < 3) {
_showErrorDialog('Nom d\'utilisateur trop court', 'Le nom d\'utilisateur doit contenir au minimum 3 caractères.');
return false;
}
return true;
}
// CORRIGÉ: Méthode _updateUser avec validation et gestion d'erreurs améliorée
Future<void> _updateUser() async {
if (!_validateFields() || _isLoading) return;
@ -101,6 +145,9 @@ class _EditUserPageState extends State<EditUserPage> {
});
try {
print("🔄 Début de la mise à jour utilisateur...");
// Créer l'objet utilisateur avec toutes les données
final updatedUser = Users(
id: widget.user.id,
name: _nameController.text.trim(),
@ -112,14 +159,69 @@ class _EditUserPageState extends State<EditUserPage> {
: widget.user.password,
roleId: _selectedRole!.id!,
roleName: _selectedRole!.designation,
pointDeVenteId: _selectedPointDeVente?['id'],
);
await AppDatabase.instance.updateUser(updatedUser);
if (mounted) _showSuccessDialog();
print("📝 Données utilisateur à mettre à jour:");
print(" ID: ${updatedUser.id}");
print(" Nom: ${updatedUser.name} ${updatedUser.lastName}");
print(" Email: ${updatedUser.email}");
print(" Username: ${updatedUser.username}");
print(" Role ID: ${updatedUser.roleId}");
print(" Point de vente ID: ${updatedUser.pointDeVenteId}");
// Validation avant mise à jour
final validationError = await AppDatabase.instance.validateUserUpdate(updatedUser);
if (validationError != null) {
if (mounted) {
_showErrorDialog('Erreur de validation', validationError);
}
return;
}
// Vérifier que l'utilisateur existe
final userExists = await AppDatabase.instance.userExists(widget.user.id!);
if (!userExists) {
if (mounted) {
_showErrorDialog('Erreur', 'L\'utilisateur à modifier n\'existe plus dans la base de données.');
}
return;
}
// Effectuer la mise à jour
final affectedRows = await AppDatabase.instance.updateUser(updatedUser);
if (affectedRows > 0) {
print("✅ Utilisateur mis à jour avec succès!");
if (mounted) _showSuccessDialog();
} else {
print("⚠️ Aucune ligne affectée lors de la mise à jour");
if (mounted) {
_showErrorDialog('Information', 'Aucune modification n\'a été effectuée.');
}
}
} catch (e) {
print('Erreur de mise à jour: $e');
print('Erreur de mise à jour: $e');
if (mounted) {
_showErrorDialog('Échec', 'Une erreur est survenue lors de la mise à jour.');
String errorMessage = 'Une erreur est survenue lors de la mise à jour.';
// Messages d'erreur plus spécifiques
if (e.toString().contains('Duplicate entry')) {
if (e.toString().contains('email')) {
errorMessage = 'Cet email est déjà utilisé par un autre utilisateur.';
} else if (e.toString().contains('username')) {
errorMessage = 'Ce nom d\'utilisateur est déjà utilisé.';
} else {
errorMessage = 'Ces informations sont déjà utilisées par un autre utilisateur.';
}
} else if (e.toString().contains('Cannot add or update a child row')) {
errorMessage = 'Le rôle ou le point de vente sélectionné n\'existe pas.';
} else if (e.toString().contains('Connection')) {
errorMessage = 'Erreur de connexion à la base de données. Veuillez réessayer.';
}
_showErrorDialog('Échec', errorMessage);
}
} finally {
if (mounted) {
@ -130,15 +232,29 @@ class _EditUserPageState extends State<EditUserPage> {
}
}
void _showSuccessDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Mise à jour réussie'),
content: const Text('Les informations de l\'utilisateur ont été mises à jour.'),
title: Row(
children: const [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(width: 8),
Text('Mise à jour réussie'),
],
),
content: const Text('Les informations de l\'utilisateur ont été mises à jour avec succès.'),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
Navigator.of(context).pop(); // Fermer le dialog
Navigator.of(context).pop(); // Retourner à la page précédente
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('OK'),
)
],
@ -150,11 +266,21 @@ class _EditUserPageState extends State<EditUserPage> {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
title: Row(
children: [
const Icon(Icons.error, color: Colors.red),
const SizedBox(width: 8),
Text(title),
],
),
content: Text(message),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('OK'),
)
],
@ -171,50 +297,144 @@ class _EditUserPageState extends State<EditUserPage> {
iconTheme: const IconThemeData(color: Colors.white),
centerTitle: true,
),
body: _isLoadingRoles
? const Center(child: CircularProgressIndicator())
body: _isLoadingData
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement des données...'),
],
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
const Icon(Icons.edit, size: 64, color: Colors.blue),
const SizedBox(height: 16),
// En-tête
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.edit, size: 32, color: Colors.blue),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Modification d\'utilisateur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
Text(
'ID: ${widget.user.id}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
],
),
),
const SizedBox(height: 24),
// Informations personnelles
_buildSectionTitle('Informations personnelles'),
const SizedBox(height: 12),
_buildTextField(_nameController, 'Prénom', Icons.person),
const SizedBox(height: 12),
_buildTextField(_lastNameController, 'Nom', Icons.person_outline),
const SizedBox(height: 12),
_buildTextField(_emailController, 'Email', Icons.email, keyboardType: TextInputType.emailAddress),
const SizedBox(height: 24),
// Informations de connexion
_buildSectionTitle('Informations de connexion'),
const SizedBox(height: 12),
_buildTextField(_usernameController, 'Nom d\'utilisateur', Icons.account_circle),
const SizedBox(height: 12),
_buildTextField(
_passwordController,
'Mot de passe (laisser vide si inchangé)',
'Nouveau mot de passe (optionnel)',
Icons.lock,
obscureText: true,
),
const SizedBox(height: 24),
// Permissions et affectation
_buildSectionTitle('Permissions et affectation'),
const SizedBox(height: 12),
_buildRoleDropdown(),
const SizedBox(height: 12),
_buildDropdown(),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _updateUser,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
_buildPointDeVenteDropdown(),
const SizedBox(height: 32),
// Boutons d'action
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: Colors.grey),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Annuler'),
),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Mettre à jour', style: TextStyle(color: Colors.white, fontSize: 16)),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _isLoading ? null : _updateUser,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Mettre à jour',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
],
)
],
),
@ -224,6 +444,26 @@ class _EditUserPageState extends State<EditUserPage> {
);
}
Widget _buildSectionTitle(String title) {
return Container(
width: double.infinity,
padding: const EdgeInsets.only(bottom: 8),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey, width: 0.5),
),
),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color.fromARGB(255, 4, 54, 95),
),
),
);
}
Widget _buildTextField(
TextEditingController controller,
String label,
@ -235,21 +475,31 @@ class _EditUserPageState extends State<EditUserPage> {
controller: controller,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
prefixIcon: Icon(icon, color: Colors.grey.shade600),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF0015B7), width: 2),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: keyboardType,
obscureText: obscureText,
);
}
Widget _buildDropdown() {
Widget _buildRoleDropdown() {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade50,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Role>(
@ -268,7 +518,7 @@ class _EditUserPageState extends State<EditUserPage> {
value: role,
child: Row(
children: [
const Icon(Icons.badge, size: 20),
Icon(Icons.badge, size: 20, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text(role.designation),
],
@ -279,4 +529,72 @@ class _EditUserPageState extends State<EditUserPage> {
),
);
}
Widget _buildPointDeVenteDropdown() {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade50,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Map<String, dynamic>?>(
value: _selectedPointDeVente,
isExpanded: true,
hint: const Text('Sélectionner un point de vente (optionnel)'),
onChanged: _isLoading
? null
: (Map<String, dynamic>? newValue) {
setState(() {
_selectedPointDeVente = newValue;
});
},
items: [
// Option "Aucun point de vente"
const DropdownMenuItem<Map<String, dynamic>?>(
value: null,
child: Row(
children: [
Icon(Icons.not_interested, size: 20, color: Colors.grey),
SizedBox(width: 8),
Text('Aucun point de vente'),
],
),
),
// Points de vente disponibles
..._pointsDeVente.map((pointDeVente) {
return DropdownMenuItem<Map<String, dynamic>>(
value: pointDeVente,
child: Row(
children: [
Icon(Icons.store, size: 20, color: Colors.grey.shade600),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(pointDeVente['nom'] ?? 'N/A'),
if (pointDeVente['code'] != null)
Text(
'Code: ${pointDeVente['code']}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
),
),
],
),
);
}).toList(),
],
),
),
);
}
}

7
lib/Views/gestionProduct.dart

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Components/appDrawer.dart';
import '../Models/produit.dart';
import '../Services/productDatabase.dart';
// import '../Services/productDatabase.dart';
import 'editProduct.dart';
import 'dart:io';
class GestionProduit extends StatelessWidget {
final ProductDatabase _productDatabase = ProductDatabase.instance;
final AppDatabase _productDatabase = AppDatabase.instance;
GestionProduit({super.key});
@ -17,7 +18,7 @@ class GestionProduit extends StatelessWidget {
final screenWidth = MediaQuery.of(context).size.width * 0.8;
return Scaffold(
appBar: const CustomAppBar(title: 'Gestion des produits'),
appBar: CustomAppBar(title: 'Gestion des produits'),
drawer: CustomDrawer(),
body: FutureBuilder<List<Product>>(
future: _productDatabase.getProducts(),

462
lib/Views/gestionRole.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Models/Permission.dart';
import 'package:youmazgestion/Services/app_database.dart';
//import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class HandleUserRole extends StatefulWidget {
const HandleUserRole({super.key});
@ -28,62 +29,177 @@ class _HandleUserRoleState extends State<HandleUserRole> {
}
Future<void> _initData() async {
final roleList = await db.getRoles();
final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
try {
final roleList = await db.getRoles();
final perms = await db.getAllPermissions();
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
// Récupération mise à jour des menus avec gestion d'erreur
final menuList = await db.getAllMenus();
for (var role in roleList) {
final roleId = role.id!;
tempRoleMenuPermissionsMap[roleId] = {};
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(roleId, menuId);
for (var role in roleList) {
final roleId = role.id!;
tempRoleMenuPermissionsMap[roleId] = {};
tempRoleMenuPermissionsMap[roleId]![menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
for (var menu in menuList) {
final menuId = menu['id'] as int;
final menuPerms = await db.getPermissionsForRoleAndMenu(roleId, menuId);
tempRoleMenuPermissionsMap[roleId]![menuId] = {
for (var perm in perms)
perm.name: menuPerms.any((mp) => mp.name == perm.name)
};
}
}
}
setState(() {
roles = roleList;
permissions = perms;
menus = menuList;
roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
});
setState(() {
roles = roleList;
permissions = perms;
menus = menuList;
roleMenuPermissionsMap = tempRoleMenuPermissionsMap;
});
} catch (e) {
print("Erreur lors de l'initialisation des données: $e");
// Afficher un message d'erreur à l'utilisateur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du chargement des données: $e'),
backgroundColor: Colors.red,
),
);
}
}
Future<void> _addRole() async {
String designation = _roleController.text.trim();
if (designation.isEmpty) return;
if (designation.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez saisir une désignation pour le rôle'),
backgroundColor: Colors.orange,
),
);
return;
}
try {
// Vérifier si le rôle existe déjà
final existingRoles = roles.where((r) => r.designation.toLowerCase() == designation.toLowerCase());
if (existingRoles.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ce rôle existe déjà'),
backgroundColor: Colors.orange,
),
);
return;
}
await db.createRole(Role(designation: designation));
_roleController.clear();
await _initData();
await db.createRole(Role(designation: designation));
_roleController.clear();
await _initData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Rôle "$designation" créé avec succès'),
backgroundColor: Colors.green,
),
);
} catch (e) {
print("Erreur lors de la création du rôle: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du rôle: $e'),
backgroundColor: Colors.red,
),
);
}
}
Future<void> _onPermissionToggle(int roleId, int menuId, String permission, bool enabled) async {
final perm = permissions.firstWhere((p) => p.name == permission);
try {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
await db.assignRoleMenuPermission(roleId, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(roleId, menuId, perm.id!);
}
setState(() {
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
});
} catch (e) {
print("Erreur lors de la modification de la permission: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la modification de la permission: $e'),
backgroundColor: Colors.red,
),
);
}
}
if (enabled) {
await db.assignRoleMenuPermission(roleId, menuId, perm.id!);
} else {
await db.removeRoleMenuPermission(roleId, menuId, perm.id!);
Future<void> _deleteRole(Role role) async {
// Empêcher la suppression du Super Admin
if (role.designation == 'Super Admin') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossible de supprimer le rôle Super Admin'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() {
roleMenuPermissionsMap[roleId]![menuId]![permission] = enabled;
});
// Demander confirmation
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text('Êtes-vous sûr de vouloir supprimer le rôle "${role.designation}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
try {
await db.deleteRole(role.id);
await _initData();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Rôle "${role.designation}" supprimé avec succès'),
backgroundColor: Colors.green,
),
);
} catch (e) {
print("Erreur lors de la suppression du rôle: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la suppression du rôle: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: "Gestion des rôles"),
appBar: CustomAppBar(title: "Gestion des rôles"),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -103,28 +219,52 @@ class _HandleUserRoleState extends State<HandleUserRole> {
controller: _roleController,
decoration: InputDecoration(
labelText: 'Nouveau rôle',
hintText: 'Ex: Manager, Vendeur...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onSubmitted: (_) => _addRole(),
),
),
const SizedBox(width: 10),
ElevatedButton(
ElevatedButton.icon(
onPressed: _addRole,
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Ajouter'),
),
],
),
),
),
const SizedBox(height: 20),
// Affichage des statistiques
if (roles.isNotEmpty)
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('Rôles', roles.length.toString(), Icons.people),
_buildStatItem('Permissions', permissions.length.toString(), Icons.security),
_buildStatItem('Menus', menus.length.toString(), Icons.menu),
],
),
),
),
const SizedBox(height: 20),
// Tableau des rôles et permissions
if (roles.isNotEmpty && permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
@ -136,69 +276,183 @@ class _HandleUserRoleState extends State<HandleUserRole> {
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width - 32,
),
child: Column(
children: menus.map((menu) {
final menuId = menu['id'] as int;
return Column(
children: [
Text(
menu['name'],
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
DataTable(
columnSpacing: 20,
columns: [
const DataColumn(
label: Text(
'Rôles',
style: TextStyle(fontWeight: FontWeight.bold),
),
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: menus.map((menu) {
final menuId = menu['id'] as int;
final menuName = menu['name'] as String;
final menuRoute = menu['route'] as String;
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
...permissions.map((perm) => DataColumn(
child: Row(
children: [
Icon(Icons.menu, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menuName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue.shade700,
),
),
Text(
menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 20,
headingRowHeight: 50,
dataRowHeight: 60,
columns: [
const DataColumn(
label: Text(
perm.name,
style: const TextStyle(fontWeight: FontWeight.bold),
'Rôles',
style: TextStyle(fontWeight: FontWeight.bold),
),
)).toList(),
],
rows: roles.map((role) {
final roleId = role.id!;
return DataRow(
cells: [
DataCell(Text(role.designation)),
...permissions.map((perm) {
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
return DataCell(
Checkbox(
value: isChecked,
onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
},
),
);
}).toList(),
),
...permissions.map((perm) => DataColumn(
label: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
perm.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
)).toList(),
const DataColumn(
label: Text(
'Actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
);
}).toList(),
),
],
);
}).toList(),
),
rows: roles.map((role) {
final roleId = role.id!;
return DataRow(
cells: [
DataCell(
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: role.designation == 'Super Admin'
? Colors.red.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Text(
role.designation,
style: TextStyle(
fontWeight: FontWeight.w500,
color: role.designation == 'Super Admin'
? Colors.red.shade700
: Colors.blue.shade700,
),
),
),
),
...permissions.map((perm) {
final isChecked = roleMenuPermissionsMap[roleId]?[menuId]?[perm.name] ?? false;
return DataCell(
Checkbox(
value: isChecked,
onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
},
activeColor: Colors.green,
),
);
}).toList(),
DataCell(
role.designation != 'Super Admin'
? IconButton(
icon: Icon(Icons.delete, color: Colors.red.shade600),
tooltip: 'Supprimer le rôle',
onPressed: () => _deleteRole(role),
)
: Icon(Icons.lock, color: Colors.grey.shade400),
),
],
);
}).toList(),
),
),
],
),
),
);
}).toList(),
),
),
),
),
)
else
const Expanded(
Expanded(
child: Center(
child: Text('Aucun rôle, permission ou menu trouvé'),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey.shade400),
const SizedBox(height: 16),
Text(
'Aucune donnée disponible',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Rôles: ${roles.length} | Permissions: ${permissions.length} | Menus: ${menus.length}',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initData,
icon: const Icon(Icons.refresh),
label: const Text('Actualiser'),
),
],
),
),
),
],
@ -206,4 +460,34 @@ class _HandleUserRoleState extends State<HandleUserRole> {
),
);
}
Widget _buildStatItem(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, size: 32, color: Colors.blue.shade600),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
);
}
@override
void dispose() {
_roleController.dispose();
super.dispose();
}
}

65
lib/Views/gestionStock.dart

@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.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/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class GestionStockPage extends StatefulWidget {
const GestionStockPage({super.key});
@ -14,10 +15,11 @@ class GestionStockPage extends StatefulWidget {
}
class _GestionStockPageState extends State<GestionStockPage> {
final ProductDatabase _database = ProductDatabase.instance;
final AppDatabase _database = AppDatabase.instance;
List<Product> _products = [];
List<Product> _filteredProducts = [];
String? _selectedCategory;
int? _selectedIdPointDeVente; // Nouveau filtre
final TextEditingController _searchController = TextEditingController();
bool _showLowStockOnly = false;
bool _sortByStockAscending = false;
@ -45,10 +47,12 @@ class _GestionStockPageState extends State<GestionStockPage> {
(product.reference?.toLowerCase().contains(query) ?? false);
final matchesCategory = _selectedCategory == null ||
product.category == _selectedCategory;
final matchesPointDeVente = _selectedIdPointDeVente == null ||
product.pointDeVenteId == _selectedIdPointDeVente; // Nouveau filtre
final matchesStockFilter = !_showLowStockOnly ||
(product.stock ?? 0) <= 5; // Seuil pour stock faible
return matchesSearch && matchesCategory && matchesStockFilter;
return matchesSearch && matchesCategory && matchesPointDeVente && matchesStockFilter;
}).toList();
// Trier les produits
@ -79,7 +83,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: 'Gestion des Stocks'),
appBar: CustomAppBar(title: 'Gestion des Stocks'),
drawer: CustomDrawer(),
body: Column(
children: [
@ -111,7 +115,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
),
const SizedBox(height: 12),
// Filtres
// Filtres - Première ligne
Row(
children: [
// Filtre par catégorie
@ -155,6 +159,53 @@ class _GestionStockPageState extends State<GestionStockPage> {
),
const SizedBox(width: 12),
// Filtre par point de vente
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _database.getPointsDeVente(), // Vous devez implémenter cette méthode
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final pointsDeVente = snapshot.data!;
return DropdownButtonFormField<int>(
value: _selectedIdPointDeVente,
decoration: InputDecoration(
labelText: 'Point de vente',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
items: [
const DropdownMenuItem<int>(
value: null,
child: Text('Tous les points de vente'),
),
...pointsDeVente.map((point) {
return DropdownMenuItem<int>(
value: point['id'],
child: Text(point['nom'] ?? 'Point ${point['id']}'),
);
}),
],
onChanged: (value) {
setState(() {
_selectedIdPointDeVente = value;
_filterProducts();
});
},
);
},
),
),
],
),
const SizedBox(height: 12),
// Filtres - Deuxième ligne
Row(
children: [
// Toggle pour stock faible seulement
Container(
decoration: BoxDecoration(
@ -329,8 +380,8 @@ class _GestionStockPageState extends State<GestionStockPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Catégorie: ${product.category}'),
if (product.reference != null && product.reference!.isNotEmpty)
Text('Réf: ${product.reference!}'),
if (product.pointDeVentelib != null)
Text('Point de vente: ${product.pointDeVentelib}'),
],
),
trailing: Row(

1121
lib/Views/gestion_point_de_vente.dart

File diff suppressed because it is too large

1042
lib/Views/historique.dart

File diff suppressed because it is too large

356
lib/Views/historique_sorties_personnelles_page.dart

@ -0,0 +1,356 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
class HistoriqueSortiesPersonnellesPage extends StatefulWidget {
const HistoriqueSortiesPersonnellesPage({super.key});
@override
_HistoriqueSortiesPersonnellesPageState createState() => _HistoriqueSortiesPersonnellesPageState();
}
class _HistoriqueSortiesPersonnellesPageState extends State<HistoriqueSortiesPersonnellesPage> {
final AppDatabase _database = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
List<Map<String, dynamic>> _historique = [];
String? _filtreStatut;
bool _isLoading = false;
bool _afficherSeulementMesDemandes = false;
@override
void initState() {
super.initState();
_loadHistorique();
}
Future<void> _loadHistorique() async {
setState(() => _isLoading = true);
try {
final historique = await _database.getHistoriqueSortiesPersonnelles(
adminId: _afficherSeulementMesDemandes ? _userController.userId : null,
statut: _filtreStatut,
pointDeVenteId: _userController.pointDeVenteId == 0 ? null : _userController.pointDeVenteId, // Si pointDeVenteId = 0, ne pas filtrer
limit: 100,
);
setState(() {
_historique = historique;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
Get.snackbar('Erreur', 'Impossible de charger l\'historique: $e');
}
}
Color _getStatutColor(String statut) {
switch (statut) {
case 'en_attente':
return Colors.orange;
case 'approuvee':
return Colors.green;
case 'refusee':
return Colors.red;
default:
return Colors.grey;
}
}
String _getStatutText(String statut) {
switch (statut) {
case 'en_attente':
return 'En attente';
case 'approuvee':
return 'Approuvée';
case 'refusee':
return 'Refusée';
default:
return statut;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Historique sorties personnelles'),
drawer: CustomDrawer(),
body: Column(
children: [
// Filtres
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey.shade50,
child: Column(
children: [
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _filtreStatut,
decoration: const InputDecoration(
labelText: 'Filtrer par statut',
border: OutlineInputBorder(),
isDense: true,
),
items: const [
DropdownMenuItem(value: null, child: Text('Tous les statuts')),
DropdownMenuItem(value: 'en_attente', child: Text('En attente')),
DropdownMenuItem(value: 'approuvee', child: Text('Approuvées')),
DropdownMenuItem(value: 'refusee', child: Text('Refusées')),
],
onChanged: (value) {
setState(() {
_filtreStatut = value;
});
_loadHistorique();
},
),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: _loadHistorique,
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Actualiser'),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _afficherSeulementMesDemandes,
onChanged: (value) {
setState(() {
_afficherSeulementMesDemandes = value ?? false;
});
_loadHistorique();
},
),
const Text('Afficher seulement mes demandes'),
],
),
],
),
),
// Liste
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadHistorique,
child: _historique.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucun historique trouvé pour ce point de vente',
style: TextStyle(fontSize: 18, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _historique.length,
itemBuilder: (context, index) {
final sortie = _historique[index];
return _buildHistoriqueCard(sortie);
},
),
),
),
],
),
);
}
Widget _buildHistoriqueCard(Map<String, dynamic> sortie) {
final dateSortie = DateTime.parse(sortie['date_sortie'].toString());
final statut = sortie['statut'].toString();
final statutColor = _getStatutColor(statut);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: statutColor.withOpacity(0.3), width: 1),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec statut et date
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statutColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: statutColor.withOpacity(0.5)),
),
child: Text(
_getStatutText(statut),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: statutColor,
),
),
),
const Spacer(),
Text(
DateFormat('dd/MM/yyyy HH:mm').format(dateSortie),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 12),
// Informations principales
Row(
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sortie['produit_nom'].toString(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text('Réf: ${sortie['produit_reference'] ?? 'N/A'}'),
Text('Quantité: ${sortie['quantite']}'),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Demandeur:',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
Text(
'${sortie['admin_nom']} ${sortie['admin_nom_famille'] ?? ''}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
),
],
),
const SizedBox(height: 12),
// Motif
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Motif:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
Text(sortie['motif'].toString()),
],
),
),
// Informations d'approbation/refus
if (statut != 'en_attente' && sortie['approbateur_nom'] != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: statutColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
statut == 'approuvee' ? 'Approuvé par:' : 'Refusé par:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
Text('${sortie['approbateur_nom']} ${sortie['approbateur_nom_famille'] ?? ''}'),
if (sortie['date_approbation'] != null)
Text(
'Le ${DateFormat('dd/MM/yyyy HH:mm').format(DateTime.parse(sortie['date_approbation'].toString()))}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
],
// Notes supplémentaires
if (sortie['notes'] != null && sortie['notes'].toString().isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Notes:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
Text(
sortie['notes'].toString(),
style: const TextStyle(fontSize: 12),
),
],
),
),
],
],
),
),
);
}
}

7
lib/Views/listCommandeHistory.dart

@ -5,6 +5,7 @@ import 'package:youmazgestion/Views/voirPlus.dart';
import 'package:youmazgestion/controller/HistoryController.dart';
import '../Models/Order.dart';
import 'package:youmazgestion/Views/detailHistory.dart';
import 'package:intl/intl.dart';
class HistoryDetailPage extends StatelessWidget {
final DateTime selectedDate;
@ -32,7 +33,7 @@ class HistoryDetailPage extends StatelessWidget {
init: controller,
builder: (controller) {
return Scaffold(
appBar: const CustomAppBar(title: 'Historique de la journée'),
appBar: CustomAppBar(title: 'Historique de la journée'),
body: Column(
children: [
Padding(
@ -112,7 +113,7 @@ class HistoryDetailPage extends StatelessWidget {
),
),
Text(
'Total Somme: $totalSum MGA',
'Total Somme: ${NumberFormat('#,##0', 'fr_FR').format(totalSum)} MGA',
style: const TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
@ -197,7 +198,7 @@ class HistoryDetailPage extends StatelessWidget {
),
],
),
subtitle: Text('Total: ${order.totalPrice} MGA'),
subtitle: Text('Total: ${NumberFormat('#,##0', 'fr_FR').format(order.totalPrice)} MGA'),
trailing: Text('Date: ${order.dateTime}'),
leading: Text('vendeur: ${order.user}'),
onTap: () {

700
lib/Views/listUser.dart

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import '../Components/app_bar.dart';
import '../Services/app_database.dart';
import 'editUser.dart';
class ListUserPage extends StatefulWidget {
@ -14,112 +14,654 @@ class ListUserPage extends StatefulWidget {
class _ListUserPageState extends State<ListUserPage> {
List<Users> userList = [];
List<Users> filteredUserList = [];
List<Map<String, dynamic>> pointsDeVente = [];
bool isLoading = true;
String searchQuery = '';
int? selectedPointDeVenteFilter;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
getUsersFromDatabase();
_loadData();
_searchController.addListener(_filterUsers);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> getUsersFromDatabase() async {
Future<void> _loadData() async {
setState(() {
isLoading = true;
});
try {
List<Users> users = await AppDatabase.instance.getAllUsers();
// Charger les utilisateurs et points de vente en parallèle
final futures = await Future.wait([
AppDatabase.instance.getAllUsers(),
AppDatabase.instance.getPointsDeVente(),
]);
final users = futures[0] as List<Users>;
final points = futures[1] as List<Map<String, dynamic>>;
setState(() {
userList = users;
filteredUserList = users;
pointsDeVente = points;
isLoading = false;
});
} catch (e) {
print(e);
print('Erreur lors du chargement: $e');
setState(() {
isLoading = false;
});
_showErrorSnackbar('Erreur lors du chargement des données');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: 'Liste des utilisateurs'),
body: ListView.builder(
itemCount: userList.length,
itemBuilder: (context, index) {
Users user = userList[index];
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shadowColor: Colors.deepOrange,
borderOnForeground: true,
child: ListTile(
title: Text(
"${user.name} ${user.lastName}",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
void _filterUsers() {
final query = _searchController.text.toLowerCase();
setState(() {
searchQuery = query;
filteredUserList = userList.where((user) {
final matchesSearch = query.isEmpty ||
user.name.toLowerCase().contains(query) ||
user.lastName.toLowerCase().contains(query) ||
user.username.toLowerCase().contains(query) ||
user.email.toLowerCase().contains(query) ||
(user.roleName?.toLowerCase().contains(query) ?? false);
final matchesPointDeVente = selectedPointDeVenteFilter == null ||
user.pointDeVenteId == selectedPointDeVenteFilter;
return matchesSearch && matchesPointDeVente;
}).toList();
});
}
void _onPointDeVenteFilterChanged(int? pointDeVenteId) {
setState(() {
selectedPointDeVenteFilter = pointDeVenteId;
});
_filterUsers();
}
String _getPointDeVenteName(int? pointDeVenteId) {
if (pointDeVenteId == null) return 'Aucun';
try {
final point = pointsDeVente.firstWhere((p) => p['id'] == pointDeVenteId);
return point['nom'] ?? 'Inconnu';
} catch (e) {
return 'Inconnu';
}
}
Future<void> _deleteUser(Users user, int index) async {
// Vérifier si l'utilisateur peut être supprimé
final canDelete = await _checkCanDeleteUser(user);
if (!canDelete['canDelete']) {
_showCannotDeleteDialog(user, canDelete['reason']);
return;
}
// Afficher la confirmation de suppression
final confirmed = await _showDeleteConfirmation(user);
if (!confirmed) return;
try {
await AppDatabase.instance.deleteUser(user.id!);
setState(() {
userList.removeWhere((u) => u.id == user.id);
filteredUserList.removeWhere((u) => u.id == user.id);
});
_showSuccessSnackbar('Utilisateur supprimé avec succès');
} catch (e) {
print('Erreur lors de la suppression: $e');
_showErrorSnackbar('Erreur lors de la suppression de l\'utilisateur');
}
}
Future<Map<String, dynamic>> _checkCanDeleteUser(Users user) async {
// Ici vous pouvez ajouter des vérifications métier
// Par exemple, vérifier si l'utilisateur a des commandes en cours, etc.
// Pour l'instant, on autorise la suppression sauf pour le Super Admin
if (user.roleName?.toLowerCase() == 'super admin') {
return {
'canDelete': false,
'reason': 'Le Super Admin ne peut pas être supprimé pour des raisons de sécurité.'
};
}
// Vérifier s'il y a des commandes associées à cet utilisateur
try {
final db = AppDatabase.instance;
final commandes = await db.database.then((connection) =>
connection.query('SELECT COUNT(*) as count FROM commandes WHERE commandeurId = ? OR validateurId = ?',
[user.id, user.id])
);
final commandeCount = commandes.first['count'] as int;
if (commandeCount > 0) {
return {
'canDelete': false,
'reason': 'Cet utilisateur a $commandeCount commande(s) associée(s). Impossible de le supprimer.'
};
}
} catch (e) {
print('Erreur lors de la vérification des contraintes: $e');
}
return {'canDelete': true, 'reason': ''};
}
Future<bool> _showDeleteConfirmation(Users user) async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: const [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Text("Confirmer la suppression"),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Êtes-vous sûr de vouloir supprimer cet utilisateur ?"),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
subtitle: Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text("Nom: ${user.name} ${user.lastName}",
style: const TextStyle(fontWeight: FontWeight.w500)),
Text("Username: ${user.username}"),
const SizedBox(height: 4),
Text("Privilège: ${user.role}"),
Text("Email: ${user.email}"),
Text("Rôle: ${user.roleName ?? 'N/A'}"),
Text("Point de vente: ${_getPointDeVenteName(user.pointDeVenteId)}"),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
),
const SizedBox(height: 12),
const Text(
"Cette action est irréversible.",
style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("Annuler"),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text("Supprimer"),
),
],
),
) ?? false;
}
void _showCannotDeleteDialog(Users user, String reason) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: const [
Icon(Icons.block, color: Colors.red),
SizedBox(width: 8),
Text("Suppression impossible"),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"L'utilisateur ${user.name} ${user.lastName} ne peut pas être supprimé.",
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.delete),
color: Colors.red,
onPressed: () {
// Action de suppression
// Vous pouvez appeler une méthode de suppression appropriée ici
// confirmation de suppression
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Supprimer"),
content: const Text(
"Êtes-vous sûr de vouloir supprimer cet utilisateur?"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Annuler"),
const Icon(Icons.info, color: Colors.red, size: 20),
const SizedBox(width: 8),
Expanded(child: Text(reason)),
],
),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text("Compris"),
),
],
),
);
}
void _showUserDetails(Users user) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Détails de ${user.name} ${user.lastName}"),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailRow("ID", "${user.id}"),
_buildDetailRow("Prénom", user.name),
_buildDetailRow("Nom", user.lastName),
_buildDetailRow("Username", user.username),
_buildDetailRow("Email", user.email),
_buildDetailRow("Rôle", user.roleName ?? 'N/A'),
_buildDetailRow("Point de vente", _getPointDeVenteName(user.pointDeVenteId)),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Fermer"),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
Get.to(() => EditUserPage(user: user))?.then((_) => _loadData());
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text("Modifier"),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
"$label:",
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(child: Text(value)),
],
),
);
}
void _showSuccessSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
Text(message),
],
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3),
),
);
}
void _showErrorSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
Text(message),
],
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(title: 'Liste des utilisateurs'),
body: isLoading
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement des utilisateurs...'),
],
),
)
: Column(
children: [
// Barre de recherche et filtres
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey.shade50,
child: Column(
children: [
// Barre de recherche
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par nom, username, email...',
prefixIcon: const Icon(Icons.search),
suffixIcon: searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.white,
),
),
const SizedBox(height: 12),
// Filtre par point de vente
Row(
children: [
const Icon(Icons.filter_list, color: Colors.grey),
const SizedBox(width: 8),
const Text('Point de vente:'),
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<int?>(
value: selectedPointDeVenteFilter,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
),
filled: true,
fillColor: Colors.white,
),
TextButton(
onPressed: () async {
await AppDatabase.instance
.deleteUser(user.id!);
Navigator.of(context).pop();
setState(() {
userList.removeAt(index);
});
},
child: const Text("Supprimer"),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Tous'),
),
...pointsDeVente.map((point) => DropdownMenuItem<int?>(
value: point['id'] as int,
child: Text(point['nom'] ?? 'N/A'),
)),
],
onChanged: _onPointDeVenteFilterChanged,
),
),
],
),
],
),
),
// Statistiques
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.blue.shade50,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${userList.length} utilisateur(s)',
style: const TextStyle(fontWeight: FontWeight.w500),
),
if (filteredUserList.length != userList.length)
Text(
'Affichés: ${filteredUserList.length}',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
),
// Liste des utilisateurs
Expanded(
child: filteredUserList.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
searchQuery.isNotEmpty ? Icons.search_off : Icons.people_outline,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
searchQuery.isNotEmpty
? 'Aucun utilisateur trouvé'
: 'Aucun utilisateur dans la base de données',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
],
);
},
);
},
),
)
: RefreshIndicator(
onRefresh: _loadData,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: filteredUserList.length,
itemBuilder: (context, index) {
Users user = filteredUserList[index];
return _buildUserCard(user, index);
},
),
),
),
],
),
);
}
Widget _buildUserCard(Users user, int index) {
final pointDeVenteName = _getPointDeVenteName(user.pointDeVenteId);
final isSuperAdmin = user.roleName?.toLowerCase() == 'super admin';
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: isSuperAdmin
? BorderSide(color: Colors.orange.shade300, width: 1)
: BorderSide.none,
),
child: InkWell(
onTap: () => _showUserDetails(user),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec nom et badge
Row(
children: [
CircleAvatar(
backgroundColor: isSuperAdmin ? Colors.orange.shade100 : Colors.blue.shade100,
child: Icon(
isSuperAdmin ? Icons.admin_panel_settings : Icons.person,
color: isSuperAdmin ? Colors.orange.shade700 : Colors.blue.shade700,
),
),
IconButton(
icon: const Icon(Icons.edit),
color: Colors.blue,
onPressed: () {
// Action de modification
// Vous pouvez naviguer vers la page de modification avec les détails de l'utilisateur
// en utilisant Navigator.push ou showDialog, selon votre besoin
Get.to(() => EditUserPage(user: user));
},
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${user.name} ${user.lastName}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
"@${user.username}",
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
if (isSuperAdmin)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade300),
),
child: Text(
'ADMIN',
style: TextStyle(
color: Colors.orange.shade800,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
},
const SizedBox(height: 12),
// Informations détaillées
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoChip(Icons.email, user.email),
const SizedBox(height: 4),
_buildInfoChip(Icons.badge, user.roleName ?? 'N/A'),
const SizedBox(height: 4),
_buildInfoChip(Icons.store, pointDeVenteName),
],
),
),
// Boutons d'actions
Column(
children: [
IconButton(
icon: const Icon(Icons.visibility, size: 20),
color: Colors.blue.shade600,
onPressed: () => _showUserDetails(user),
tooltip: 'Voir les détails',
),
IconButton(
icon: const Icon(Icons.edit, size: 20),
color: Colors.green.shade600,
onPressed: () {
Get.to(() => EditUserPage(user: user))?.then((_) => _loadData());
},
tooltip: 'Modifier',
),
IconButton(
icon: Icon(
isSuperAdmin ? Icons.lock : Icons.delete,
size: 20,
),
color: isSuperAdmin ? Colors.grey : Colors.red.shade600,
onPressed: isSuperAdmin
? null
: () => _deleteUser(user, index),
tooltip: isSuperAdmin ? 'Protection Super Admin' : 'Supprimer',
),
],
),
],
),
],
),
),
),
);
}
}
Widget _buildInfoChip(IconData icon, String text) {
return Row(
children: [
Icon(icon, size: 14, color: Colors.grey.shade600),
const SizedBox(width: 6),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade700,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}

636
lib/Views/loginPage.dart

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Services/PermissionCacheService.dart'; // Nouveau import
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/Views/mobilepage.dart';
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/Services/app_database.dart';
import '../Models/users.dart';
import '../controller/userController.dart';
@ -19,9 +20,12 @@ class _LoginPageState extends State<LoginPage> {
late TextEditingController _usernameController;
late TextEditingController _passwordController;
final UserController userController = Get.put(UserController());
final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau
bool _isErrorVisible = false;
bool _isLoading = false;
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
String _loadingMessage = 'Connexion en cours...'; // Nouveau
@override
void initState() {
@ -34,19 +38,7 @@ class _LoginPageState extends State<LoginPage> {
void checkUserCount() async {
try {
final userCount = await AppDatabase.instance.getUserCount();
print('Nombre d\'utilisateurs trouvés: $userCount'); // Debug
// Commentez cette partie pour permettre le login même sans utilisateurs
/*
if (userCount == 0) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
);
}
}
*/
print('Nombre d\'utilisateurs trouvés: $userCount');
} catch (error) {
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
setState(() {
@ -63,228 +55,480 @@ class _LoginPageState extends State<LoginPage> {
super.dispose();
}
Future<void> saveUserData(Users user, String role, int userId) async {
try {
// CORRECTION : Utiliser la nouvelle méthode du contrôleur
// Le contrôleur se charge maintenant de tout (observable + SharedPreferences)
userController.setUserWithCredentials(user, role, userId);
// /// OPTIMISÉ: Sauvegarde avec préchargement des permissions
// Future<void> saveUserData(Users user, String role, int userId) async {
// try {
// userController.setUserWithCredentials(user, role, userId);
print('Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId');
} catch (error) {
print('Erreur lors de la sauvegarde: $error');
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
}
// if (user.pointDeVenteId != null) {
// await userController.loadPointDeVenteDesignation();
// }
// print('✅ Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
// } catch (error) {
// print('❌ Erreur lors de la sauvegarde: $error');
// throw Exception('Erreur lors de la sauvegarde des données utilisateur');
// }
// }
/// NOUVEAU: Préchargement des permissions en arrière-plan
Future<void> _preloadUserPermissions(String username) async {
try {
setState(() {
_loadingMessage = 'Préparation du menu...';
});
// Lancer le préchargement en parallèle avec les autres tâches
final permissionFuture = _cacheService.preloadUserData(username);
// Attendre maximum 2 secondes pour les permissions
await Future.any([
permissionFuture,
Future.delayed(const Duration(seconds: 2))
]);
print('✅ Permissions préparées (ou timeout)');
} catch (e) {
print('⚠️ Erreur préchargement permissions: $e');
// Continuer même en cas d'erreur
}
}
void _login() async {
if (_isLoading) return;
/// OPTIMISÉ: Connexion avec préchargement parallèle
void _login() async {
if (_isLoading) return;
final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim();
final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim();
// Validation basique
if (username.isEmpty || password.isEmpty) {
setState(() {
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_isErrorVisible = true;
});
return;
}
if (username.isEmpty || password.isEmpty) {
setState(() {
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_isErrorVisible = true;
});
return;
}
setState(() {
_isLoading = true;
_isErrorVisible = false;
_loadingMessage = 'Connexion...';
});
try {
print('🔐 Tentative de connexion pour: $username');
final dbInstance = AppDatabase.instance;
// 1. Vérification rapide de la base
setState(() {
_isLoading = true;
_isErrorVisible = false;
_loadingMessage = 'Vérification...';
});
try {
print('Tentative de connexion pour: $username');
final userCount = await dbInstance.getUserCount();
print('✅ Base accessible, $userCount utilisateurs');
} catch (dbError) {
throw Exception('Base de données inaccessible: $dbError');
}
// 2. Vérification des identifiants
setState(() {
_loadingMessage = 'Authentification...';
});
// Vérification de la connexion à la base de données
final dbInstance = AppDatabase.instance;
bool isValidUser = await dbInstance.verifyUser(username, password);
// Test de connexion à la base
try {
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');
}
if (isValidUser) {
setState(() {
_loadingMessage = 'Chargement du profil...';
});
// 3. Récupération parallèle des données
final futures = await Future.wait([
dbInstance.getUser(username),
dbInstance.getUserCredentials(username, password),
]);
final user = futures[0] as Users;
final userCredentials = futures[1] as Map<String, dynamic>?;
if (userCredentials != null) {
print('✅ Connexion réussie pour: ${user.username}');
print(' Rôle: ${userCredentials['role']}');
setState(() {
_loadingMessage = 'Préparation...';
});
// 4. Sauvegarde des données utilisateur
await saveUserData(
user,
userCredentials['role'] as String,
userCredentials['id'] as int,
);
// 5. Préchargement des permissions EN PARALLÈLE avec la navigation
setState(() {
_loadingMessage = 'Finalisation...';
});
// Lancer le préchargement en arrière-plan SANS attendre
_cacheService.preloadUserDataAsync(username);
// Vérifier les identifiants
bool isValidUser = await dbInstance.verifyUser(username, password);
print('Résultat de la vérification: $isValidUser');
if (isValidUser) {
// Récupérer les informations complètes de l'utilisateur
Users user = await dbInstance.getUser(username);
print('Utilisateur récupéré: ${user.username}');
// Récupérer les credentials
Map<String, dynamic>? userCredentials =
await dbInstance.getUserCredentials(username, password);
if (userCredentials != null) {
print('Connexion réussie pour: ${user.username}');
print('Rôle: ${userCredentials['role']}');
print('ID: ${userCredentials['id']}');
// CORRECTION : Sauvegarder ET mettre à jour le contrôleur
await saveUserData(
user,
userCredentials['role'] as String,
userCredentials['id'] as int,
);
// Navigation
if (mounted) {
// 6. Navigation immédiate
if (mounted) {
if (userCredentials['role'] == 'commercial' || userCredentials['role'] == 'caisse') {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
MaterialPageRoute(builder: (context) => const MainLayout()),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => DashboardPage()),
);
}
} else {
throw Exception('Erreur lors de la récupération des credentials');
}
// Les permissions se chargeront en arrière-plan après la navigation
print('🚀 Navigation immédiate, permissions en arrière-plan');
} else {
print('Identifiants invalides pour: $username');
setState(() {
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true;
});
throw Exception('Erreur lors de la récupération des credentials');
}
} catch (error) {
print('Erreur lors de la connexion: $error');
} else {
setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}';
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true;
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
} catch (error) {
setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}';
_isErrorVisible = true;
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
_loadingMessage = 'Connexion en cours...';
});
}
}
}
/// OPTIMISÉ: Sauvegarde rapide
Future<void> saveUserData(Users user, String role, int userId) async {
try {
userController.setUserWithCredentials(user, role, userId);
// Charger le point de vente en parallèle si nécessaire
if (user.pointDeVenteId != null) {
// Ne pas attendre, charger en arrière-plan
unawaited(userController.loadPointDeVenteDesignation());
}
print('✅ Utilisateur sauvegardé rapidement');
} catch (error) {
print('❌ Erreur lors de la sauvegarde: $error');
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
}
}
@override
Widget build(BuildContext context) {
final Color primaryBlue = const Color(0xFF0033A1);
final Color accentRed = const Color(0xFFD70000);
final Color secondaryBlue = const Color(0xFF1976D2);
final Color primaryColor = primaryBlue;
final Color accentColor = secondaryBlue;
final Color cardColor = Colors.white;
return Scaffold(
appBar: AppBar(
title: const Text(
'Login',
style: TextStyle(color: Colors.white),
),
backgroundColor: const Color.fromARGB(255, 4, 54, 95),
centerTitle: true,
),
backgroundColor: primaryColor,
body: ParticleBackground(
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(30.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: const Icon(
Icons.lock_outline,
size: 100.0,
color: Color.fromARGB(255, 4, 54, 95),
child: SingleChildScrollView(
child: Container(
width: MediaQuery.of(context).size.width < 500
? double.infinity
: 400,
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
decoration: BoxDecoration(
color: cardColor.withOpacity(0.98),
borderRadius: BorderRadius.circular(30.0),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.2),
blurRadius: 16,
spreadRadius: 4,
offset: const Offset(0, 8),
),
),
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Username',
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Center(
child: Column(
children: [
CircleAvatar(
radius: 38,
backgroundColor: accentColor.withOpacity(0.15),
child: Icon(
Icons.lock_outline,
color: accentColor,
size: 50,
),
),
const SizedBox(height: 14),
Text(
'GUYCOM',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 28,
),
),
const SizedBox(height: 4),
Text(
'Connectez-vous à votre compte',
style: TextStyle(
color: primaryColor.withOpacity(.8),
fontSize: 16,
),
),
],
),
),
),
const SizedBox(height: 16.0),
TextField(
controller: _passwordController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
const SizedBox(height: 24),
// Username Field
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Nom d\'utilisateur',
labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
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,
onSubmitted: (_) => _login(),
),
const SizedBox(height: 16.0),
Visibility(
visible: _isErrorVisible,
child: Text(
_errorMessage,
style: const TextStyle(
color: Colors.red,
fontSize: 14,
const SizedBox(height: 18.0),
// Password Field
TextField(
controller: _passwordController,
enabled: !_isLoading,
obscureText: true,
decoration: InputDecoration(
labelText: 'Mot de passe',
labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
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 (_isLoading) ...[
const SizedBox(height: 16.0),
Column(
children: [
// Barre de progression animée
Container(
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: accentColor.withOpacity(0.2),
),
child: LayoutBuilder(
builder: (context, constraints) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: constraints.maxWidth * 0.7,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
gradient: LinearGradient(
colors: [accentColor, accentColor.withOpacity(0.7)],
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7),
elevation: 5.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
minimumSize: const Size(double.infinity, 48),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
);
},
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(accentColor),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
_loadingMessage,
style: TextStyle(
color: accentColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.left,
),
),
],
),
const SizedBox(height: 4),
Text(
"Le menu se chargera en arrière-plan",
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
],
// Error Message
if (_isErrorVisible) ...[
const SizedBox(height: 12.0),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage,
style: const TextStyle(
color: Colors.red,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontSize: 16,
],
),
),
],
const SizedBox(height: 26.0),
// Login Button
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
disabledBackgroundColor: accentColor.withOpacity(0.3),
foregroundColor: Colors.white,
elevation: 7.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
minimumSize: const Size(double.infinity, 52),
),
child: _isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
const SizedBox(width: 12),
Text(
'Connexion...',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: .4,
),
),
),
),
// Bouton de debug (à supprimer en production)
if (_isErrorVisible)
TextButton(
onPressed: () async {
try {
final count = await AppDatabase.instance.getUserCount();
print('Debug: $count utilisateurs dans la base');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$count utilisateurs trouvés')),
);
} catch (e) {
print('Debug error: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
},
child: const Text('Debug: Vérifier BDD'),
),
],
// Debug Button (à enlever en production)
if (_isErrorVisible && !_isLoading) ...[
const SizedBox(height: 8),
TextButton(
onPressed: () async {
try {
final count = await AppDatabase.instance.getUserCount();
final stats = _cacheService.getCacheStats();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'BDD: $count utilisateurs\n'
'Cache: ${stats['users_cached']} utilisateurs en cache',
),
duration: const Duration(seconds: 3),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
},
child: Text(
'Debug: Vérifier BDD & Cache',
style: TextStyle(
color: primaryColor.withOpacity(0.6),
fontSize: 12,
),
),
),
],
],
),
),
),
),

44
lib/Views/mobilepage.dart

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/appDrawer.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> {
// Index par défaut pour la page de commande
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: CustomDrawer(),
body: const HistoriquePage(),
);
}
}

4928
lib/Views/newCommand.dart

File diff suppressed because it is too large

63
lib/Views/produitsCard.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:youmazgestion/Models/produit.dart';
import 'package:intl/intl.dart';
class ProductCard extends StatefulWidget {
final Product product;
@ -16,7 +17,8 @@ class ProductCard extends StatefulWidget {
State<ProductCard> createState() => _ProductCardState();
}
class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin {
class _ProductCardState extends State<ProductCard>
with TickerProviderStateMixin {
int selectedQuantity = 1;
late AnimationController _scaleController;
late AnimationController _fadeController;
@ -122,7 +124,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
: _buildPlaceholderImage(),
),
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
@ -141,7 +142,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
),
),
if (widget.product.isStockDefined())
Positioned(
top: 12,
@ -183,7 +183,6 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
),
),
Positioned(
left: 0,
right: 0,
@ -201,7 +200,8 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
vertical: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
@ -222,7 +222,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
const SizedBox(height: 4),
Text(
'${widget.product.price.toStringAsFixed(2)} FCFA',
'${NumberFormat('#,##0', 'fr_FR').format(widget.product.price)} MGA',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
@ -239,9 +239,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
],
),
),
const SizedBox(height: 12),
Row(
children: [
Container(
@ -250,7 +248,8 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color:
Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
@ -295,9 +294,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
],
),
),
const SizedBox(width: 8),
Expanded(
child: MouseRegion(
cursor: SystemMouseCursors.click,
@ -306,9 +303,11 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: () {
widget.onAddToCart(widget.product, selectedQuantity);
widget.onAddToCart(widget.product,
selectedQuantity);
ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Row(
children: [
@ -320,16 +319,20 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
Expanded(
child: Text(
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
overflow: TextOverflow.ellipsis,
overflow: TextOverflow
.ellipsis,
),
),
],
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
duration:
const Duration(seconds: 1),
behavior:
SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius:
BorderRadius.circular(10),
),
),
);
@ -342,21 +345,27 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color.fromARGB(255, 4, 54, 95),
Color.fromARGB(255, 6, 80, 140),
Color.fromARGB(
255, 4, 54, 95),
Color.fromARGB(
255, 6, 80, 140),
],
),
borderRadius: BorderRadius.circular(20),
borderRadius:
BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3),
color: const Color.fromARGB(
255, 4, 54, 95)
.withOpacity(0.3),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
Icons.add_shopping_cart,
@ -369,10 +378,12 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
'Ajouter',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontWeight:
FontWeight.bold,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
overflow:
TextOverflow.ellipsis,
),
),
],
@ -442,7 +453,9 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
child: Icon(
icon,
size: 16,
color: onPressed != null ? const Color.fromARGB(255, 4, 54, 95) : Colors.grey,
color: onPressed != null
? const Color.fromARGB(255, 4, 54, 95)
: Colors.grey,
),
),
),

140
lib/Views/registrationPage.dart

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import '../Services/app_database.dart'; // Changé de authDatabase.dart
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key});
@ -23,7 +24,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
Role? _selectedRole;
bool _isLoading = false;
bool _isLoadingRoles = true;
List<Map<String, dynamic>> _availablePointsDeVente = [];
int? _selectedPointDeVenteId;
bool _isLoadingPointsDeVente = true;
@override
void initState() {
super.initState();
@ -36,19 +39,38 @@ class _RegistrationPageState extends State<RegistrationPage> {
_initializeDatabase();
}
Future<void> _initializeDatabase() async {
try {
await AppDatabase.instance.initDatabase();
await _loadRoles();
} catch (error) {
print('Erreur lors de l\'initialisation: $error');
if (mounted) {
_showErrorDialog('Erreur d\'initialisation',
'Impossible d\'initialiser l\'application. Veuillez redémarrer.');
}
Future<void> _initializeDatabase() async {
try {
await AppDatabase.instance.initDatabase();
await _loadRoles();
await _loadPointsDeVente(); // Ajouté ici
} catch (error) {
print('Erreur lors de l\'initialisation: $error');
if (mounted) {
_showErrorDialog('Erreur d\'initialisation',
'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 {
try {
final roles = await AppDatabase.instance.getRoles();
@ -98,15 +120,16 @@ class _RegistrationPageState extends State<RegistrationPage> {
}
bool _validateFields() {
if (_nameController.text.trim().isEmpty ||
_lastNameController.text.trim().isEmpty ||
_emailController.text.trim().isEmpty ||
_usernameController.text.trim().isEmpty ||
_passwordController.text.trim().isEmpty ||
_selectedRole == null) {
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
return false;
}
if (_nameController.text.trim().isEmpty ||
_lastNameController.text.trim().isEmpty ||
_emailController.text.trim().isEmpty ||
_usernameController.text.trim().isEmpty ||
_passwordController.text.trim().isEmpty ||
_selectedRole == null ||
_selectedPointDeVenteId == null) { // Ajouté ici
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs.');
return false;
}
// Validation basique de l'email
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(_emailController.text.trim())) {
@ -126,23 +149,24 @@ class _RegistrationPageState extends State<RegistrationPage> {
void _register() async {
if (_isLoading) return;
if (!_validateFields()) return;
if (!_validateFields()) return;
setState(() {
_isLoading = true;
});
setState(() {
_isLoading = true;
});
try {
// Créer l'objet utilisateur avec le nouveau modèle
final Users user = Users(
name: _nameController.text.trim(),
lastName: _lastNameController.text.trim(),
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
username: _usernameController.text.trim(),
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
roleName: _selectedRole!.designation, // Pour l'affichage
);
try {
// Créer l'objet utilisateur avec le nouveau modèle
final Users user = Users(
name: _nameController.text.trim(),
lastName: _lastNameController.text.trim(),
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
username: _usernameController.text.trim(),
roleId: _selectedRole!.id!, // Utiliser l'ID du rôle
roleName: _selectedRole!.designation, // Pour l'affichage
pointDeVenteId: _selectedPointDeVenteId, // Ajouté ici
);
// Sauvegarder l'utilisateur dans la base de données
final int userId = await AppDatabase.instance.createUser(user);
@ -191,7 +215,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
MaterialPageRoute(builder: (context) => DashboardPage()),
);
},
child: const Text('OK'),
@ -361,6 +385,46 @@ class _RegistrationPageState extends State<RegistrationPage> {
),
),
),
// 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['nom']),
],
),
);
}).toList(),
),
),
),
const SizedBox(height: 16.0),
const SizedBox(height: 24.0),
SizedBox(
width: double.infinity,

140
lib/Views/ticketPage.dart

@ -1,10 +1,9 @@
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:esc_pos_printer/esc_pos_printer.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:esc_pos_utils/esc_pos_utils.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
@ -31,117 +30,6 @@ class TicketPage extends StatelessWidget {
required this.amountPaid,
}) : super(key: key);
Future<void> _printTicket() async {
final profile = await CapabilityProfile.load();
final printer = NetworkPrinter(PaperSize.mm80, profile);
printer.text('Ticket de caisse',
styles: const PosStyles(
align: PosAlign.center,
height: PosTextSize.size2,
width: PosTextSize.size2,
));
printer.text('Entreprise : $businessName');
printer.text('Adresse : $businessAddress');
printer.text('Numéro de téléphone : $businessPhoneNumber');
printer.hr();
printer.row([
PosColumn(
text: 'Produit',
width: 3,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: 'Quantité',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: 'Prix unitaire',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: 'Total',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
]);
printer.hr();
for (final cartItem in cartItems) {
final product = cartItem.product;
final quantity = cartItem.quantity;
final productTotal = product.price * quantity;
printer.row([
PosColumn(
text: product.name,
width: 3,
),
PosColumn(
text: quantity.toString(),
width: 1,
),
PosColumn(
text: '${product.price.toStringAsFixed(2)} MGA',
width: 1,
),
PosColumn(
text: '${productTotal.toStringAsFixed(2)} MGA',
width: 1,
),
]);
}
printer.hr();
printer.row([
PosColumn(
text: 'Total :',
width: 3,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
PosColumn(
text: '${totalCartPrice.toStringAsFixed(2)} MGA',
width: 1,
styles: const PosStyles(align: PosAlign.left, bold: true),
),
]);
printer.row([
PosColumn(
text: 'Somme remise :',
width: 3,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: '${amountPaid.toStringAsFixed(2)} MGA',
width: 1,
styles: const PosStyles(align: PosAlign.left),
),
]);
printer.row([
PosColumn(
text: 'Somme rendue :',
width: 3,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: '${(amountPaid - totalCartPrice).toStringAsFixed(2)} MGA',
width: 1,
styles: const PosStyles(align: PosAlign.left),
),
]);
printer.hr();
printer.text('Youmaz vous remercie pour votre achat!!!');
printer.feed(2);
printer.cut();
printer.disconnect(); // Fermez la connexion après l'impression
Get.snackbar('Impression', 'Ticket imprimé avec succès');
}
Future<void> _generateAndSavePDF() async {
final pdf = pw.Document();
@ -179,8 +67,8 @@ class TicketPage extends StatelessWidget {
return [
product.name,
quantity.toString(),
'${product.price.toStringAsFixed(2)} MGA',
'${productTotal.toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(product.price)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(productTotal)} MGA',
];
}).toList(),
],
@ -194,7 +82,7 @@ class TicketPage extends StatelessWidget {
pw.Text('Total :',
style: pw.TextStyle(
fontSize: 18, fontWeight: pw.FontWeight.bold)),
pw.Text('${totalCartPrice.toStringAsFixed(2)} MGA',
pw.Text('${NumberFormat('#,##0.00', 'fr_FR').format(totalCartPrice)} MGA',
style: pw.TextStyle(
fontSize: 18, fontWeight: pw.FontWeight.bold)),
],
@ -207,7 +95,7 @@ class TicketPage extends StatelessWidget {
children: [
pw.Text('Somme remise :',
style: const pw.TextStyle(fontSize: 16)),
pw.Text('${amountPaid.toStringAsFixed(2)} MGA',
pw.Text('${NumberFormat('#,##0.00', 'fr_FR').format(amountPaid)} MGA',
style: const pw.TextStyle(fontSize: 16)),
],
),
@ -218,7 +106,7 @@ class TicketPage extends StatelessWidget {
pw.Text('Somme rendue :',
style: const pw.TextStyle(fontSize: 16)),
pw.Text(
'${(amountPaid - totalCartPrice).toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(amountPaid - totalCartPrice)} MGA',
style: const pw.TextStyle(fontSize: 16)),
],
),
@ -265,11 +153,7 @@ class TicketPage extends StatelessWidget {
}
// Obtenir la date actuelle
final currentDate = DateTime.now();
final formattedDate = DateFormat('dd/MM/yyyy HH:mm').format(currentDate);
// Calculer la somme remise
final double discount = totalOrderAmount - totalCartPrice;
// Calculer la somme rendue
final double change = amountPaid - totalOrderAmount;
@ -387,14 +271,14 @@ class TicketPage extends StatelessWidget {
TableCell(
child: Center(
child: Text(
'${product.price.toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(product.price)} MGA',
),
),
),
TableCell(
child: Center(
child: Text(
'${productTotal.toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(productTotal)} MGA',
),
),
),
@ -421,7 +305,7 @@ class TicketPage extends StatelessWidget {
),
),
Text(
'${totalOrderAmount.toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(totalOrderAmount)} MGA',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -446,7 +330,7 @@ class TicketPage extends StatelessWidget {
),
),
Text(
'${amountPaid.toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(amountPaid)} MGA',
style: const TextStyle(
fontSize: 16,
),
@ -464,7 +348,7 @@ class TicketPage extends StatelessWidget {
),
),
Text(
'${change.toStringAsFixed(2)} MGA',
'${NumberFormat('#,##0.00', 'fr_FR').format(change)} MGA',
style: const TextStyle(
fontSize: 16,
),

10
lib/accueil.dart

@ -1,8 +1,8 @@
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:get/get.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/produitsCard.dart';
import 'Components/appDrawer.dart';
@ -10,7 +10,7 @@ import 'Components/app_bar.dart';
import 'Components/cartItem.dart';
import 'Models/produit.dart';
import 'Services/OrderDatabase.dart';
import 'Services/productDatabase.dart';
//import 'Services/productDatabase.dart';
import 'Views/ticketPage.dart';
import 'controller/userController.dart';
import 'my_app.dart';
@ -25,7 +25,7 @@ class AccueilPage extends StatefulWidget {
class _AccueilPageState extends State<AccueilPage> {
final UserController userController = Get.put(UserController());
final ProductDatabase productDatabase = ProductDatabase();
final AppDatabase productDatabase = AppDatabase.instance;
late Future<Map<String, List<Product>>> productsFuture;
final OrderDatabase orderDatabase = OrderDatabase.instance;
final WorkDatabase workDatabase = WorkDatabase.instance;
@ -114,7 +114,7 @@ class _AccueilPageState extends State<AccueilPage> {
await orderDatabase.insertOrderItem(
orderId, product.name, quantity, price);
final updatedStock = product.stock! - quantity;
final updatedStock = product.stock - quantity;
await productDatabase.updateStock(product.id!, updatedStock);
}
@ -448,7 +448,7 @@ class _AccueilPageState extends State<AccueilPage> {
fontSize: 16,
fontWeight: FontWeight.bold)),
Text(
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} MGA',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,

101
lib/config/DatabaseConfig.dart

@ -0,0 +1,101 @@
// Config/database_config.dart
import 'dart:io';
import 'dart:async';
class DatabaseConfig {
// Local MySQL settings
static const String localHost = '192.168.88.3';
static const String localUsername = 'guycom';
static const String? localPassword = '3iV59wjRdbuXAPR';
static const String localDatabase = 'guycom';
// static const String localHost = 'localhost';
// static const String localUsername = 'root';
// static const String? localPassword = null;
// static const String localDatabase = 'guycom';
// Production (public) MySQL settings
static const String prodHost = '102.16.56.177';
// static const String prodHost = '185.70.105.157';
static const String prodUsername = 'guycom';
static const String prodPassword = '3iV59wjRdbuXAPR';
static const String prodDatabase = 'guycom';
static const int port = 3306;
static const Duration connectionTimeout = Duration(seconds: 30);
static const Duration queryTimeout = Duration(seconds: 15);
static const int maxConnections = 10;
static const int minConnections = 2;
static bool get isDevelopment => true;
/// Build config map for connection
static Map<String, dynamic> _buildConfig({
required String host,
required String user,
required String? password,
required String database,
}) {
return {
'host': host,
'port': port,
'user': user,
'password': password,
'database': database,
'timeout': connectionTimeout.inSeconds,
};
}
/// TCP check if MySQL server is reachable
static Future<bool> isServerReachable(String host, {int port = 3306}) async {
try {
final socket =
await Socket.connect(host, port, timeout: Duration(seconds: 2));
socket.destroy();
return true;
} catch (_) {
return false;
}
}
/// Get smart config (local if reachable, otherwise public)
static Future<Map<String, dynamic>> getSmartConfig() async {
if (await isServerReachable(localHost, port: port)) {
return _buildConfig(
host: localHost,
user: localUsername,
password: localPassword,
database: localDatabase,
);
} else {
return _buildConfig(
host: prodHost,
user: prodUsername,
password: prodPassword,
database: prodDatabase,
);
}
}
/// Validate any config
static bool validateConfig(Map<String, dynamic> config) {
try {
return config['host']?.toString().isNotEmpty == true &&
config['database']?.toString().isNotEmpty == true &&
config['user'] != null;
} catch (e) {
print("Erreur de validation de la configuration: $e");
return false;
}
}
/// Add retry config
static Map<String, dynamic> addRetry(Map<String, dynamic> config) {
return {
...config,
'retryCount': 3,
'retryDelay': 5000, // ms
};
}
}

5
lib/controller/AccueilController.dart

@ -1,17 +1,18 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
import '../Components/cartItem.dart';
import '../Models/produit.dart';
import '../Services/OrderDatabase.dart';
import '../Services/WorkDatabase.dart';
import '../Services/productDatabase.dart';
//import '../Services/productDatabase.dart';
import '../Views/ticketPage.dart';
import '../my_app.dart';
class AccueilController extends GetxController {
final UserController userController = Get.find();
final ProductDatabase productDatabase = ProductDatabase();
final AppDatabase productDatabase = AppDatabase.instance;
final Rx<Map<String, List<Product>>> productsFuture = Rx({}); // Observable
final OrderDatabase orderDatabase = OrderDatabase.instance;
final WorkDatabase workDatabase = WorkDatabase.instance;

31
lib/controller/HistoryController.dart

@ -27,8 +27,6 @@ class HistoryController extends GetxController {
getTotalSumOrdersByMonth(DateTime.now());
getOrdersByMonth(DateTime.now());
getOrderCountByMonth(DateTime.now());
}
Future<void> fetchOrders() async {
@ -60,9 +58,6 @@ class HistoryController extends GetxController {
}
}
List<DateTime?> getDistinctDates() {
final currentDate = DateTime.now();
final dates = [...orders.map((order) => order.startDate), currentDate]
@ -73,7 +68,9 @@ class HistoryController extends GetxController {
List<Order> filterOrdersByDateRange(DateTime startDate) {
final filteredList = orders
.where((order) => order.startDate != null && order.startDate!.isAtSameMomentAs(startDate))
.where((order) =>
order.startDate != null &&
order.startDate!.isAtSameMomentAs(startDate))
.toList();
filteredOrders.value = filteredList;
return filteredList;
@ -81,7 +78,8 @@ class HistoryController extends GetxController {
Future<List<Order>> getOrdersByStartDate(DateTime startDate) async {
try {
final ordersByStartDate = await _orderDatabase.getOrdersByStartDate(startDate);
final ordersByStartDate =
await _orderDatabase.getOrdersByStartDate(startDate);
return ordersByStartDate;
} catch (e) {
print(e);
@ -89,22 +87,23 @@ class HistoryController extends GetxController {
}
}
double getTotalSumOrdersByStartDate (DateTime startDate) {
double getTotalSumOrdersByStartDate(DateTime startDate) {
final filteredOrders = filterOrdersByDateRange(startDate);
double totalSum = filteredOrders.fold(0, (sum, order) => sum + order.totalPrice);
double totalSum =
filteredOrders.fold(0, (sum, order) => sum + order.totalPrice);
return totalSum;
}
Future<RxDouble> getTotalSumOrdersByMonth(DateTime date) async {
final filteredOrders = await getOrdersByMonth(date);
double totalsum = filteredOrders.fold(0, (sum, order) => sum + order.totalPrice);
double totalsum =
filteredOrders.fold(0, (sum, order) => sum + order.totalPrice);
totalSum.value = totalsum;
print(totalSum.value);
// print(totalSum.value);
return totalSum;
}
Future<Map<String, int>> getProductQuantitiesByDate(DateTime date) async {
Future<Map<String, int>> getProductQuantitiesByDate(DateTime date) async {
try {
return await _orderDatabase.getProductQuantitiesByDate(date);
} catch (e) {
@ -153,16 +152,10 @@ class HistoryController extends GetxController {
}
}
Future<int> getOrderCountByMonth(DateTime month) async {
List<Order> orders = await getOrdersByMonth(month);
int orderCount = orders.length;
orderQuantity.value = orderCount;
return orderCount;
}
}

203
lib/controller/userController.dart

@ -1,7 +1,8 @@
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Services/PermissionCacheService.dart';
class UserController extends GetxController {
final _username = ''.obs;
@ -10,8 +11,14 @@ class UserController extends GetxController {
final _name = ''.obs;
final _lastname = ''.obs;
final _password = ''.obs;
final _userId = 0.obs; // Ajout de l'ID utilisateur
final _userId = 0.obs;
final _pointDeVenteId = 0.obs;
final _pointDeVenteDesignation = ''.obs;
// Cache service
final PermissionCacheService _cacheService = PermissionCacheService.instance;
// Getters
String get username => _username.value;
String get email => _email.value;
String get role => _role.value;
@ -19,14 +26,16 @@ class UserController extends GetxController {
String get lastname => _lastname.value;
String get password => _password.value;
int get userId => _userId.value;
int get pointDeVenteId => _pointDeVenteId.value;
String get pointDeVenteDesignation => _pointDeVenteDesignation.value;
@override
void onInit() {
super.onInit();
loadUserData(); // Charger les données au démarrage
loadUserData();
}
// CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
/// SIMPLIFIÉ: Charge les données utilisateur sans cache persistant
Future<void> loadUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
@ -34,64 +43,103 @@ class UserController extends GetxController {
final storedUsername = prefs.getString('username') ?? '';
final storedRole = prefs.getString('role') ?? '';
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) {
try {
// Récupérer les données complètes depuis la base de données
Users user = await AppDatabase.instance.getUser(storedUsername);
// Mettre à jour TOUTES les données
_username.value = user.username;
_email.value = user.email;
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
_role.value = storedRole; // Récupéré depuis SharedPreferences
_userId.value = storedUserId; // Récupéré depuis SharedPreferences
_role.value = storedRole;
_userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
print("✅ Données chargées depuis la DB - Username: ${_username.value}");
print("✅ Name: ${_name.value}, Email: ${_email.value}");
print("✅ Role: ${_role.value}, UserID: ${_userId.value}");
if (_pointDeVenteDesignation.value.isEmpty &&
_pointDeVenteId.value > 0) {
await loadPointDeVenteDesignation();
}
// Précharger les permissions en arrière-plan (non bloquant)
_preloadPermissionsInBackground();
} catch (dbError) {
print('❌ Erreur DB, chargement depuis SharedPreferences uniquement: $dbError');
// Fallback : charger depuis SharedPreferences uniquement
// print("❌ Erreur BDD, utilisation du fallback: $dbError");
_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;
// Précharger quand même
_preloadPermissionsInBackground();
}
}
} catch (e) {
// print('❌ Erreur lors du chargement des données utilisateur: $e');
}
}
/// Précharge les permissions en arrière-plan (non bloquant)
void _preloadPermissionsInBackground() {
if (_username.value.isNotEmpty) {
// Lancer en arrière-plan sans attendre
Future.microtask(() async {
try {
await _cacheService.preloadUserData(_username.value);
} catch (e) {
print("⚠️ Erreur préchargement permissions (non critique): $e");
}
} else {
print("❌ Aucun utilisateur stocké trouvé");
});
}
}
Future<void> loadPointDeVenteDesignation() async {
if (_pointDeVenteId.value <= 0) return;
try {
final pointDeVente =
await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
if (pointDeVente != null) {
_pointDeVenteDesignation.value = pointDeVente['nom'] as String;
await saveUserData();
}
} catch (e) {
print('❌ Erreur lors du chargement des données utilisateur: $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
/// Mise à jour avec préchargement des permissions
void setUserWithCredentials(Users user, String role, int userId) {
_username.value = user.username;
_email.value = user.email;
_role.value = role; // Rôle depuis les credentials
_role.value = role;
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
_userId.value = userId; // ID depuis les credentials
_userId.value = userId;
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
print("✅ Utilisateur mis à jour avec credentials:");
print(" Username: ${_username.value}");
print(" Name: ${_name.value}");
print(" Email: ${_email.value}");
print(" Role: ${_role.value}");
print(" UserID: ${_userId.value}");
// print("✅ Utilisateur mis à jour avec credentials:");
// print(" Username: ${_username.value}");
// print(" Role: ${_role.value}");
// print(" UserID: ${_userId.value}");
// Sauvegarder dans SharedPreferences
saveUserData();
// Précharger immédiatement les permissions après connexion
_preloadPermissionsInBackground();
}
// MÉTHODE EXISTANTE AMÉLIORÉE
void setUser(Users user) {
_username.value = user.username;
_email.value = user.email;
@ -99,17 +147,11 @@ class UserController extends GetxController {
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
// Note: _userId reste inchangé si pas fourni
print("✅ Utilisateur mis à jour (méthode legacy):");
print(" Username: ${_username.value}");
print(" Role: ${_role.value}");
// Sauvegarder dans SharedPreferences
saveUserData();
_preloadPermissionsInBackground();
}
// CORRECTION : Sauvegarder TOUTES les données importantes
Future<void> saveUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
@ -119,68 +161,95 @@ class UserController extends GetxController {
await prefs.setString('role', _role.value);
await prefs.setString('name', _name.value);
await prefs.setString('lastname', _lastname.value);
await prefs.setInt('user_id', _userId.value); // Sauvegarder l'ID
await prefs.setInt('user_id', _userId.value);
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");
} catch (e) {
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e');
print('❌ Erreur lors de la sauvegarde: $e');
}
}
// CORRECTION : Vider TOUTES les données (SharedPreferences + Observables)
/// MODIFIÉ: Vider les données ET le cache de session
Future<void> clearUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
// Vider SharedPreferences
// IMPORTANT: Vider le cache de session
_cacheService.clearAllCache();
// Effacer SharedPreferences
await prefs.remove('username');
await prefs.remove('email');
await prefs.remove('role');
await prefs.remove('name');
await prefs.remove('lastname');
await prefs.remove('user_id'); // Supprimer l'ID aussi
await prefs.remove('user_id');
await prefs.remove('point_de_vente_id');
await prefs.remove('point_de_vente_designation');
// Vider les variables observables
// Effacer les observables
_username.value = '';
_email.value = '';
_role.value = '';
_name.value = '';
_lastname.value = '';
_password.value = '';
_userId.value = 0; // Réinitialiser l'ID
_userId.value = 0;
_pointDeVenteId.value = 0;
_pointDeVenteDesignation.value = '';
print("✅ Toutes les données utilisateur ont été effacées");
print("Données utilisateur et cache de session vidés");
} catch (e) {
print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
print('❌ Erreur lors de l\'effacement: $e');
}
}
// MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté
// Getters utilitaires
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
// MÉTHODE UTILITAIRE : Obtenir le nom complet
String get fullName => '${_name.value} ${_lastname.value}'.trim();
/// OPTIMISÉ: Vérification des permissions depuis le cache de session
Future<bool> hasPermission(String permission, String route) async {
try {
if (_username.value.isEmpty) {
print('⚠️ Username vide, rechargement des données...');
print('⚠️ Username vide, rechargement...');
await loadUserData();
}
if (_username.value.isEmpty) {
print('Impossible de vérifier les permissions : utilisateur non connecté');
print('Utilisateur non connecté');
return false;
}
return await AppDatabase.instance.hasPermission(username, permission, route);
// Essayer d'abord le cache
if (_cacheService.isLoaded) {
return _cacheService.hasPermission(_username.value, permission, route);
}
// Si pas encore chargé, charger et essayer de nouveau
print("🔄 Cache non chargé, chargement des permissions...");
await _cacheService.loadUserPermissions(_username.value);
return _cacheService.hasPermission(_username.value, permission, route);
} catch (e) {
print('❌ Erreur vérification permission: $e');
return false; // Sécurité : refuser l'accès en cas d'erreur
// Fallback vers la méthode originale en cas d'erreur
try {
return await AppDatabase.instance
.hasPermission(_username.value, permission, route);
} catch (fallbackError) {
print('❌ Erreur fallback permission: $fallbackError');
return false;
}
}
}
Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
/// Vérification de permissions multiples
Future<bool> hasAnyPermission(
List<String> permissionNames, String menuRoute) async {
for (String permissionName in permissionNames) {
if (await hasPermission(permissionName, menuRoute)) {
return true;
@ -189,16 +258,40 @@ class UserController extends GetxController {
return false;
}
// MÉTHODE DEBUG : Afficher l'état actuel
/// Obtenir les menus accessibles depuis le cache
List<Map<String, dynamic>> getUserMenus() {
if (_username.value.isEmpty || !_cacheService.isLoaded) return [];
return _cacheService.getUserMenus(_username.value);
}
/// Vérifier l'accès à un menu depuis le cache
bool hasMenuAccess(String menuRoute) {
if (_username.value.isEmpty || !_cacheService.isLoaded) return false;
return _cacheService.hasMenuAccess(_username.value, menuRoute);
}
/// Forcer le rechargement des permissions (pour les admins après modification)
Future<void> refreshPermissions() async {
if (_username.value.isNotEmpty) {
await _cacheService.refreshUserPermissions(_username.value);
}
}
/// Vérifier si le cache est prêt
bool get isCacheReady => _cacheService.isLoaded && _username.value.isNotEmpty;
/// Debug
void debugPrintUserState() {
print("=== ÉTAT UTILISATEUR ===");
print("Username: ${_username.value}");
print("Name: ${_name.value}");
print("Lastname: ${_lastname.value}");
print("Email: ${_email.value}");
print("Role: ${_role.value}");
print("UserID: ${_userId.value}");
print("IsLoggedIn: $isLoggedIn");
print("Cache Ready: $isCacheReady");
print("========================");
// Debug du cache
_cacheService.debugPrintCache();
}
}

114
lib/main.dart

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
import 'Services/productDatabase.dart';
import 'my_app.dart';
import 'package:logging/logging.dart';
@ -10,30 +9,117 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
// Initialiser les bases de données une seule fois
// await AppDatabase.instance.deleteDatabaseFile();
// await ProductDatabase.instance.deleteDatabaseFile();
// print("Initialisation de l'application...");
await ProductDatabase.instance.initDatabase();
await AppDatabase.instance.initDatabase();
// // Pour le développement : supprimer toutes les tables (équivalent à deleteDatabaseFile)
// // ATTENTION: Décommentez seulement si vous voulez réinitialiser la base
// // await AppDatabase.instance.deleteDatabaseFile();
// // Initialiser la base de données MySQL
// print("Connexion à la base de données MySQL...");
// // await AppDatabase.instance.initDatabase();
// print("Base de données initialisée avec succès !");
// Afficher les informations de la base (pour debug)
await AppDatabase.instance.printDatabaseInfo();
Get.put(
UserController()); // Ajoute ce code AVANT tout accès au UserController
// Initialiser le contrôleur utilisateur
Get.put(UserController());
// print("Contrôleur utilisateur initialisé");
// Configurer le logger
setupLogger();
// print("Lancement de l'application...");
runApp(const GetMaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
));
} catch (e) {
print('Erreur lors de l\'initialisation: $e');
// Vous pourriez vouloir afficher une page d'erreur ici
// print('Erreur lors de l\'initialisation: $e');
// Afficher une page d'erreur avec plus de détails
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Text('Erreur d\'initialisation: $e'),
backgroundColor: Colors.red[50],
appBar: AppBar(
title: const Text('Erreur d\'initialisation'),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 48,
),
const SizedBox(height: 16),
const Text(
'Erreur de connexion à la base de données',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 16),
const Text(
'Vérifiez que :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('• XAMPP est démarré'),
const Text('• MySQL est en cours d\'exécution'),
const Text('• La base de données "guycom_databse" existe'),
const Text('• Les paramètres de connexion sont corrects'),
const SizedBox(height: 16),
const Text(
'Détails de l\'erreur :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey),
),
child: SingleChildScrollView(
child: Text(
e.toString(),
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Relancer l'application
main();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text('Réessayer'),
),
),
],
),
),
),
));
@ -43,6 +129,6 @@ void main() async {
void setupLogger() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
// print('${record.level.name}: ${record.time}: ${record.message}');
});
}

58
lib/my_app.dart

@ -1,8 +1,4 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'Views/ErreurPage.dart';
import 'Views/loginPage.dart';
class MyApp extends StatelessWidget {
@ -10,12 +6,11 @@ class MyApp extends StatelessWidget {
static bool isRegisterOpen = false;
static DateTime? startDate;
static late String path;
static const Gradient primaryGradient = LinearGradient(
colors: [
Colors.white,
const Color.fromARGB(255, 4, 54, 95),
Color.fromARGB(255, 4, 54, 95),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'GUYCOM',
debugShowCheckedModeBanner: false,
theme: ThemeData(
canvasColor: Colors.transparent,
),
home: Builder(
builder: (context) {
return FutureBuilder<bool>(
future:
checkLocalDatabasesExist(), // Appel à la fonction de vérification
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Affichez un indicateur de chargement si nécessaire
return const CircularProgressIndicator();
} else if (snapshot.hasError || !(snapshot.data ?? false)) {
// S'il y a une erreur ou si les bases de données n'existent pas
return ErreurPage(
dbPath:
path); // Redirigez vers la page d'erreur en affichant le chemin de la base de données
} else {
// Si les bases de données existent, affichez la page d'accueil normalement
return Container(
decoration: const BoxDecoration(
gradient: MyApp.primaryGradient,
),
child: const LoginPage(),
);
}
},
);
},
home: Container(
decoration: const BoxDecoration(
gradient: MyApp.primaryGradient,
),
child: const LoginPage(),
),
);
}
Future<bool> checkLocalDatabasesExist() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final dbPath = documentsDirectory.path;
path = dbPath;
// Vérifier si le fichier de base de données products2.db existe
final productsDBFile = File('$dbPath/products2.db');
final productsDBExists = await productsDBFile.exists();
// Vérifier si le fichier de base de données auth.db existe
final authDBFile = File('$dbPath/usersDb.db');
final authDBExists = await authDBFile.exists();
// Vérifier si d'autres bases de données nécessaires existent, le cas échéant
return productsDBExists && authDBExists;
}
}

16
linux/flutter/generated_plugin_registrant.cc

@ -6,22 +6,30 @@
#include "generated_plugin_registrant.h"
#include <charset_converter/charset_converter_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <open_file_linux/open_file_linux_plugin.h>
#include <printing/printing_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) charset_converter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "CharsetConverterPlugin");
charset_converter_plugin_register_with_registrar(charset_converter_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

4
linux/flutter/generated_plugins.cmake

@ -3,10 +3,12 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
charset_converter
file_selector_linux
open_file_linux
printing
screen_retriever
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

10
macos/Flutter/GeneratedPluginRegistrant.swift

@ -7,18 +7,24 @@ import Foundation
import file_picker
import file_selector_macos
import mobile_scanner
import open_file_mac
import path_provider_foundation
import printing
import screen_retriever
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

296
pubspec.lock

@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.12.0"
barcode:
dependency: transitive
description:
@ -49,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.9"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
boolean_selector:
dependency: transitive
description:
@ -57,6 +65,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
camera:
dependency: "direct main"
description:
name: camera
sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
url: "https://pub.dev"
source: hosted
version: "0.10.6"
camera_android:
dependency: transitive
description:
name: camera_android
sha256: "4f40d053a67e99029b5be7f00ef8047b63edb65ccc4e2546b84d47e302c6bf62"
url: "https://pub.dev"
source: hosted
version: "0.10.10+4"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: cabc6cbdeadca9cbcac178110c9d5e87bde5c646c3b8c9c4c2747fc500a12432
url: "https://pub.dev"
source: hosted
version: "0.9.20+5"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
camera_web:
dependency: transitive
description:
name: camera_web
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
url: "https://pub.dev"
source: hosted
version: "0.3.5"
camera_windows:
dependency: "direct main"
description:
name: camera_windows
sha256: c4339d71bc4256993f5c8ae2f3355463d830a5cb52851409ab1c627401c69811
url: "https://pub.dev"
source: hosted
version: "0.2.6+2"
characters:
dependency: transitive
description:
@ -65,22 +121,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
charset_converter:
charcode:
dependency: transitive
description:
name: charset_converter
sha256: a601f27b78ca86c3d88899d53059786d9c3f3c485b64974e9105c06c2569aef5
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "1.4.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c"
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.3.5"
version: "0.4.2"
clock:
dependency: transitive
description:
@ -121,14 +177,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@ -153,22 +201,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
esc_pos_printer:
dependency: "direct main"
description:
name: esc_pos_printer
sha256: "312b05f909f3f7dd1e6a3332cf384dcee2c3a635138823654cd9c0133d8b5c45"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
esc_pos_utils:
dependency: "direct main"
description:
name: esc_pos_utils
sha256: "8ec0013d7a7f1e790ced6b09b95ce3bf2c6f9468a3e2bc49ece000761d86c6f8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
excel:
dependency: "direct main"
description:
@ -181,10 +213,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
version: "1.3.2"
ffi:
dependency: transitive
description:
@ -241,6 +273,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
url: "https://pub.dev"
source: hosted
version: "0.65.0"
flutter:
dependency: "direct main"
description: flutter
@ -328,14 +376,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.8.0"
gbk_codec:
dependency: transitive
description:
name: gbk_codec
sha256: "3af5311fc9393115e3650ae6023862adf998051a804a08fb804f042724999f61"
url: "https://pub.dev"
source: hosted
version: "0.4.0"
get:
dependency: "direct main"
description:
@ -348,10 +388,10 @@ packages:
dependency: transitive
description:
name: get_it
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
url: "https://pub.dev"
source: hosted
version: "7.7.0"
version: "8.2.0"
google_fonts:
dependency: transitive
description:
@ -368,22 +408,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.2"
hex:
dependency: transitive
description:
name: hex
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
html:
dependency: transitive
description:
name: html
sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8"
url: "https://pub.dev"
source: hosted
version: "0.15.5+1"
http:
dependency: transitive
description:
@ -412,10 +436,10 @@ packages:
dependency: "direct main"
description:
name: image
sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6"
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "4.3.0"
image_picker:
dependency: "direct main"
description:
@ -508,10 +532,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
@ -608,14 +632,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
msix:
dependency: "direct main"
description:
name: msix
sha256: e3de4d9f52543ad6e4b0f534991e1303cbd379d24be28dd241ac60bd9439a201
sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5
url: "https://pub.dev"
source: hosted
version: "3.7.0"
version: "3.16.12"
mysql1:
dependency: "direct main"
description:
name: mysql1
sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9"
url: "https://pub.dev"
source: hosted
version: "0.20.0"
nested:
dependency: transitive
description:
@ -624,6 +664,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
numbers_to_letters:
dependency: "direct main"
description:
name: numbers_to_letters
sha256: "70c7ed2f04c1982a299e753101fbc2d52ed5b39a2b3dd2a9c07ba131e9c0948e"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
open_file:
dependency: "direct main"
description:
@ -772,10 +820,18 @@ packages:
dependency: "direct main"
description:
name: pdf
sha256: "10659b915e65832b106f6d1d213e09b789cc1f24bf282ee911e49db35b96be4d"
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
url: "https://pub.dev"
source: hosted
version: "3.11.3"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "3.8.4"
version: "1.0.4"
petitparser:
dependency: transitive
description:
@ -808,6 +864,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
printing:
dependency: "direct main"
description:
name: printing
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
url: "https://pub.dev"
source: hosted
version: "5.14.2"
provider:
dependency: transitive
description:
@ -832,6 +904,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_code_scanner_plus:
dependency: "direct main"
description:
name: qr_code_scanner_plus
sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
qr_flutter:
dependency: "direct main"
description:
@ -864,6 +944,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.3"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
shared_preferences:
dependency: "direct main"
description:
@ -957,22 +1045,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_common:
dependency: transitive
description:
@ -989,22 +1061,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.5"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3:
dependency: transitive
description:
@ -1029,6 +1085,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -1041,18 +1105,18 @@ packages:
dependency: "direct main"
description:
name: syncfusion_flutter_charts
sha256: bdb7cc5814ceb187793cea587f4a5946afcffd96726b219cee79df8460f44b7b
sha256: "0222ac9d8cb6c671f014effe9bd5c0aef35eadb16471355345ba87cc0ac007b3"
url: "https://pub.dev"
source: hosted
version: "21.2.4"
version: "20.4.54"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: "8db8f55c77f56968681447d3837c10f27a9e861e238a898fda116c7531def979"
sha256: "3979f0b1c5a97422cadae52d476c21fa3e0fb671ef51de6cae1d646d8b99fe1f"
url: "https://pub.dev"
source: hosted
version: "21.2.10"
version: "20.4.54"
synchronized:
dependency: transitive
description:
@ -1169,10 +1233,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "15.0.0"
version: "14.3.1"
web:
dependency: transitive
description:
@ -1182,13 +1246,21 @@ packages:
source: hosted
version: "1.1.1"
win32:
dependency: transitive
dependency: "direct main"
description:
name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
url: "https://pub.dev"
source: hosted
version: "5.12.0"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf"
url: "https://pub.dev"
source: hosted
version: "0.3.9"
xdg_directories:
dependency: transitive
description:
@ -1213,6 +1285,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.3"
zxing2:
dependency: "direct main"
description:
name: zxing2
sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f"
url: "https://pub.dev"
source: hosted
version: "0.2.4"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"
flutter: ">=3.29.0"

34
pubspec.yaml

@ -35,7 +35,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
get: ^4.6.5
sqflite: ^2.2.8+4
# sqflite: ^2.2.8+4
mysql1: ^0.20.0
flutter_dropzone: ^4.2.1
image_picker: ^0.8.7+5
@ -43,14 +44,16 @@ dependencies:
sqflite_common_ffi: ^2.2.5
quantity_input: ^1.0.2
grouped_list: ^5.1.2
esc_pos_printer: ^4.0.1
esc_pos_utils: ^1.1.0
# esc_pos_printer: ^4.0.1
win32: ^5.12.0
# esc_pos_utils: ^1.1.0
printing: ^5.10.0
flutter_login: ^4.1.1
image: ^3.0.2
image: ^4.3.0
logging: ^1.2.0
msix: ^3.7.0
flutter_charts: ^0.5.1
syncfusion_flutter_charts: ^21.2.4
syncfusion_flutter_charts: ^20.4.48
shelf: ^1.4.1
shelf_router: ^1.1.4
pdf: ^3.8.4
@ -62,13 +65,14 @@ dependencies:
path_provider: ^2.0.15
shared_preferences: ^2.2.2
excel: ^2.0.1
mobile_scanner: ^7.0.1
fl_chart: ^0.65.0
numbers_to_letters: ^1.0.0
qr_code_scanner_plus: ^2.0.10+1
window_manager: ^0.3.7
camera: ^0.10.5+9
zxing2: ^0.2.1
camera_windows: ^0.2.6+2
dev_dependencies:
flutter_test:
sdk: flutter
@ -101,6 +105,12 @@ flutter:
- assets/database/usersdb.db
- assets/database/work.db
- assets/database/roles.db
- assets/airtel_money.png
- assets/mvola.jpg
- assets/Orange_money.png
- assets/fa-solid-900.ttf
- assets/NotoEmoji-Regular.ttf
- assets/fonts/Roboto-Italic.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware

3
test/widget_test.dart

@ -7,8 +7,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:youmazgestion/my_app.dart';
import 'package:guycom/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {

15
windows/flutter/generated_plugin_registrant.cc

@ -6,15 +6,24 @@
#include "generated_plugin_registrant.h"
#include <charset_converter/charset_converter_plugin.h>
#include <camera_windows/camera_windows.h>
#include <file_selector_windows/file_selector_windows.h>
#include <printing/printing_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
CharsetConverterPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CharsetConverterPlugin"));
CameraWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CameraWindows"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

5
windows/flutter/generated_plugins.cmake

@ -3,9 +3,12 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
charset_converter
camera_windows
file_selector_windows
printing
screen_retriever
url_launcher_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

BIN
windows/runner/resources/app_icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Loading…
Cancel
Save