Compare commits

..

44 Commits

Author SHA1 Message Date
58154af680 modification 2026 2026-01-16 12:17:06 +03:00
865410ae93 recherche et regle 2026-01-16 10:55:02 +03:00
13554ee49c push 09112025 2025-11-09 12:33:37 +01:00
c757bdb701 push finale 2025-08-08 12:47:18 +02:00
4b4d9637fd 01082025_02 2025-08-01 08:05:56 +02:00
2eeaeaa4c4 01082025_01 2025-08-01 07:15:02 +02:00
f509af1716 31072025 2025-07-31 21:56:32 +02:00
ab97716dd2 push 29072025_01 2025-07-29 13:56:57 +02:00
c5ef3ca0cb commit 28072025_03 2025-07-28 22:34:55 +02:00
f41fc922a6 commit de 28072025_02 2025-07-28 18:20:54 +02:00
d043f6f20b commit de 18072025 2025-07-28 11:07:16 +02:00
13296529c3 client 2025-07-25 14:36:26 +03:00
99d570bd3a requete pour retourner point de vente 2025-07-25 13:04:58 +03:00
5ad019d35e commit de 25-07-2025 2025-07-25 11:23:39 +02:00
a52749c415 commit 24/07/2025-2 2025-07-24 21:33:58 +02:00
3e6a81c2c3 demande de sortie 2025-07-24 22:23:35 +03:00
2764111fa4 filtre sur la page demande de sortie 2025-07-24 22:00:10 +03:00
b415b9f501 qr code scanner 2025-07-24 21:46:09 +03:00
ab398bddc6 commit 24/07/2025 2025-07-24 20:32:03 +02:00
01e9cabeba scan qr 2025-07-24 20:42:51 +03:00
1f2a255719 add client also 2025-07-24 15:31:18 +03:00
3fcc50195c email non obligatoire 2025-07-24 15:29:55 +03:00
55822cd52e non obligatoire sur email 2025-07-24 15:25:38 +03:00
6b83a1ab5e database 2025-07-23 12:06:32 +03:00
ASUS
14ce881a3c cacher boutton facture 2025-07-10 08:30:16 +03:00
ASUS
6bc8373cad fixed 2025-07-10 07:41:42 +03:00
b.razafimandimbihery
551831df74 change scan 2025-06-28 23:43:38 +03:00
b.razafimandimbihery
cb59287bfd changement au niveau de scan 2025-06-28 23:41:58 +03:00
b.razafimandimbihery
55e896775d changement au nivaue de scan 2025-06-28 23:28:18 +03:00
46826006c2 nif stat 2025-06-24 13:44:56 +03:00
e02c4c8bef change the facture to show only on superadmin 2025-06-23 23:04:06 +03:00
b.razafimandimbihery
332ed228ae fix bug commande 2025-06-22 18:59:22 +03:00
b.razafimandimbihery
2af3b01d92 last update update 2025-06-20 09:46:00 +03:00
be8c169ad1 qr code reference 2025-06-16 12:30:36 +03:00
ASUS
48ae916f02 date format 2025-06-16 12:29:13 +03:00
b.razafimandimbihery
c0bbb0da2b lastlast update 2025-06-14 21:49:10 +03:00
b.razafimandimbihery
595b38e9fb adding barcode scanner 2025-06-13 23:00:16 +03:00
b.razafimandimbihery
525b09c81f scan code bar 2025-06-13 22:58:23 +03:00
b.razafimandimbihery
b5a11aa3c9 migration mysql 2025-06-10 11:52:35 +03:00
b.razafimandimbihery
831cce13da last last last update 2025-06-06 15:54:29 +03:00
b.razafimandimbihery
c8fedd08e5 last update 2025-05-31 14:24:47 +03:00
b.razafimandimbihery
9eafda610f commit fonctionnalite impec 2025-05-31 11:25:01 +03:00
b.razafimandimbihery
2bef06a2fe commit commit 2025-05-31 10:17:02 +03:00
b.razafimandimbihery
57ea91b3d7 maj dernier farany farany 2025-05-31 04:42:03 +03:00
126 changed files with 33969 additions and 5384 deletions

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

BIN
assets/Orange_money.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/airtel_money.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/fa-solid-900.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/mvola.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,4 @@
android: true
ios: true
macos: true
image_path: assets/youmaz2.png

View File

@ -32,7 +32,7 @@ target 'Runner' do
use_frameworks!
use_modular_headers!
# Add This Line
pod 'PhoneNumberKit', '~> 4.0.1'
pod 'PhoneNumberKit'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# target 'RunnerTests' do
# inherit! :search_paths

View File

@ -38,11 +38,63 @@ PODS:
- Flutter (1.0.0)
- flutter_pdfview (1.0.2):
- Flutter
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleMLKit/BarcodeScanning (6.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 5.0.0)
- GoogleMLKit/MLKitCore (6.0.0):
- MLKitCommon (~> 11.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilitiesComponents (1.1.0):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.5.0)
- image_picker_ios (0.0.1):
- Flutter
- libphonenumber_plugin (0.0.1):
- Flutter
- PhoneNumberKit
- MLImage (1.0.0-beta5)
- MLKitBarcodeScanning (5.0.0):
- MLKitCommon (~> 11.0)
- MLKitVision (~> 7.0)
- MLKitCommon (11.0.0):
- GoogleDataTransport (< 10.0, >= 9.4.1)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
- GoogleUtilitiesComponents (~> 1.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (7.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta5)
- MLKitCommon (~> 11.0)
- mobile_scanner (5.2.3):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- open_file_ios (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
@ -54,10 +106,11 @@ PODS:
- PhoneNumberKit/PhoneNumberKitCore (4.0.1)
- PhoneNumberKit/UIKit (4.0.1):
- PhoneNumberKit/PhoneNumberKitCore
- PromisesObjC (2.4.0)
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- sqflite_darwin (0.0.4):
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
@ -71,17 +124,30 @@ DEPENDENCIES:
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- libphonenumber_plugin (from `.symlinks/plugins/libphonenumber_plugin/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- PhoneNumberKit (~> 4.0.1)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- PhoneNumberKit
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GoogleUtilitiesComponents
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PhoneNumberKit
- PromisesObjC
- SDWebImage
- SwiftyGif
@ -98,12 +164,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
libphonenumber_plugin:
:path: ".symlinks/plugins/libphonenumber_plugin/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
@ -114,16 +182,29 @@ SPEC CHECKSUMS:
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
libphonenumber_plugin: d134f173b22bfa5ede50887071f087f309277f8c
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
mobile_scanner: 92e8812bf22a8f84131e2a7f9d0f44dad1a4742b
nanopb: 438bc412db1928dac798aa6fd75726007be04262
open_file_ios: 5ff7526df64e4394b4fe207636b67a95e83078bb
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PhoneNumberKit: a74155066daa6450475f6a029068eb919fb00d5d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
PODFILE CHECKSUM: a28aa98e3ca7183648527da64078769adf630e89
PODFILE CHECKSUM: 40a81d716601cb7e489b5e5da42322c08f3e9b52
COCOAPODS: 1.16.2

View File

@ -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,7 +208,24 @@
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",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
65083E199DC1B675D18EE07F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -236,7 +247,22 @@
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;
};
731B87E0F0D7405841DD9A9D /* [CP] Embed Pods Frameworks */ = {
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
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 = (
@ -253,21 +279,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* 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;

View File

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e988660f504b67cd568a692e296f07f10e5","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SwiftyGif","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"SwiftyGif","INFOPLIST_FILE":"Target Support Files/SwiftyGif/ResourceBundle-SwiftyGif-SwiftyGif-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"SwiftyGif","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e985e443d8c100c089879ec09e1edd6f9cb","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e985ef3d7c611762a00791c0dce3b9c1050","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SwiftyGif","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"SwiftyGif","INFOPLIST_FILE":"Target Support Files/SwiftyGif/ResourceBundle-SwiftyGif-SwiftyGif-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"SwiftyGif","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e982c7cb25390ded4c879095950e69476e3","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e985ef3d7c611762a00791c0dce3b9c1050","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SwiftyGif","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"SwiftyGif","INFOPLIST_FILE":"Target Support Files/SwiftyGif/ResourceBundle-SwiftyGif-SwiftyGif-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"SwiftyGif","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98b1c2c6a5fb9838908cd71b35862110e2","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e984126d34170f98c65c70def8f5b8f20ec","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98a6afbe0825a5caf22666104c86cb5308","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e988549d5cdb73a77eca3da12a5c007a47e","guid":"bfdfe7dc352907fc980b868725387e9816c93ab2ca4dc1378da7325dd567c14e"}],"guid":"bfdfe7dc352907fc980b868725387e983bd1d13fb7a36851b0c1ede064ecb5bf","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98f5cd644fc2aeb8654450a2168f52697c","name":"SwiftyGif-SwiftyGif","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9824d08d80a11ec6e34ca1c1e17eca0844","name":"SwiftyGif.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e980c993d7ddef2cd989a58977310054417","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/PhoneNumberKit","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"PhoneNumberKit","INFOPLIST_FILE":"Target Support Files/PhoneNumberKit/ResourceBundle-PhoneNumberKitPrivacy-PhoneNumberKit-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"PhoneNumberKitPrivacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9866c425b89616e6c5ec0d6996b12e2dc1","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98376c2f2df2f6bfe63740e04a95ffe2b5","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/PhoneNumberKit","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"PhoneNumberKit","INFOPLIST_FILE":"Target Support Files/PhoneNumberKit/ResourceBundle-PhoneNumberKitPrivacy-PhoneNumberKit-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"PhoneNumberKitPrivacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e981784057da42771ea9dd544b8b67ee6a6","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98376c2f2df2f6bfe63740e04a95ffe2b5","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/PhoneNumberKit","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"PhoneNumberKit","INFOPLIST_FILE":"Target Support Files/PhoneNumberKit/ResourceBundle-PhoneNumberKitPrivacy-PhoneNumberKit-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"PhoneNumberKitPrivacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9805ee69a5db135e23a1a137abf3b37883","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98a65b97a42f777915e9f5af4d3c0cc6ef","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98b9fe88cce4579466975917fbf5aea6f5","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d975c131fcea719de40a0868fbedd482","guid":"bfdfe7dc352907fc980b868725387e98ae62a96c61d566598d3f008991516132"}],"guid":"bfdfe7dc352907fc980b868725387e98eb78db11c6c8d3a25e4501a28bb3ea45","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9830efee903e29d8dc896f9a9a5aa8ca9d","name":"PhoneNumberKit-PhoneNumberKitPrivacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98bf04cb0cc0ec9fc576a20a91ae4a14e8","name":"PhoneNumberKitPrivacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98098898a87882e0ba60461ed53ee0d4e3","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/sqflite_darwin","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"sqflite_darwin","INFOPLIST_FILE":"Target Support Files/sqflite_darwin/ResourceBundle-sqflite_darwin_privacy-sqflite_darwin-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"sqflite_darwin_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98af19cace04cb499ddc25346d7f47e62a","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982b339f852b9159289fe803f2621270f6","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/sqflite_darwin","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"sqflite_darwin","INFOPLIST_FILE":"Target Support Files/sqflite_darwin/ResourceBundle-sqflite_darwin_privacy-sqflite_darwin-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"sqflite_darwin_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e982f4dccd4b9824a05c78189a9571a1980","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982b339f852b9159289fe803f2621270f6","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/sqflite_darwin","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"sqflite_darwin","INFOPLIST_FILE":"Target Support Files/sqflite_darwin/ResourceBundle-sqflite_darwin_privacy-sqflite_darwin-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"sqflite_darwin_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98bcc3274107fde69d9d7509cefb7fcd88","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98216295202790db25e9b0e34e90a1aeef","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98344c817d6c09d693bfccb3c532e743c6","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989259fb844fd2e715518b85fddf032f47","guid":"bfdfe7dc352907fc980b868725387e987c4f28e59ad920f4fca69d20cf180876"}],"guid":"bfdfe7dc352907fc980b868725387e984cfc7d039a36266507dad559262c4ee1","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9883134bb5f399cb37a1eb075d4fea30d8","name":"sqflite_darwin-sqflite_darwin_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9849c1d4b1200fcbf6f387f94121c7d0bf","name":"sqflite_darwin_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e983f9ec39d3e84781fd2f5a36ca1a80506","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/url_launcher_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"url_launcher_ios","INFOPLIST_FILE":"Target Support Files/url_launcher_ios/ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"url_launcher_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e987265dd0b465c65f58583a7f6eb722c5c","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c67589f1a2ce7a8d2b3953fd1c9e7a59","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/url_launcher_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"url_launcher_ios","INFOPLIST_FILE":"Target Support Files/url_launcher_ios/ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"url_launcher_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98770a5de6c02fc1fab4b8e0e55f7c3355","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c67589f1a2ce7a8d2b3953fd1c9e7a59","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/url_launcher_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"url_launcher_ios","INFOPLIST_FILE":"Target Support Files/url_launcher_ios/ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"url_launcher_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9881f6e4414693308b29a159a116fda4e1","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e983f1601c37c12cb60ac7bde5bccf7b7ea","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9878c0a878e6869c4a747b51e38e264060","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98294df81e7bab02110569395536fccd2e","guid":"bfdfe7dc352907fc980b868725387e9829d123b9c14728f272d2f181d228418e"}],"guid":"bfdfe7dc352907fc980b868725387e98b05ac2031006599d69596e969bf569b8","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9891b3b8cc56823cdea4b418e009a423b2","name":"url_launcher_ios-url_launcher_ios_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9827df8da513ac7d6928fc311b53a7155d","name":"url_launcher_ios_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9845c9ef11ede518490fc9d6929e898522","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"12.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e982cf0da236cf10d087750aa1434da9227","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9874d05dd0b084952d606802c8e305b268","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"12.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e98cc28f154213fd8181aa70d4c188a8335","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9874d05dd0b084952d606802c8e305b268","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"12.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e981f19fefc6e52ad9e4e005a2248234387","name":"Release"}],"buildPhases":[],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e986930a4bb647f427b124c91b4304b3779","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989e73560a7b2519b4f32ffa652fbdd236","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9811fb4b1ff39ef46a5b54abff58b39232","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98462f63a3d7aacd57b89f76dab1c799e5","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9811fb4b1ff39ef46a5b54abff58b39232","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98727c787420ee2ab7343bb57882873569","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98106ffc7884aca9a29ebf5fddf5b11607","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e989d0320a7552287e16e40195cd920f1c7","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9849193480861cca9555a074f31073df3e","guid":"bfdfe7dc352907fc980b868725387e982f77944f90f08bae32b83f1efff7d152"}],"guid":"bfdfe7dc352907fc980b868725387e9880c6d8312df72d0f8d9311053be3c0cb","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5","name":"path_provider_foundation-path_provider_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986e649604f74c414a7c2dbe5ef4cc4e75","name":"path_provider_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e989e4d61be8423b8a76e1f0acae4344c0c","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/DKPhotoGallery","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"DKPhotoGallery","INFOPLIST_FILE":"Target Support Files/DKPhotoGallery/ResourceBundle-DKPhotoGallery-DKPhotoGallery-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"DKPhotoGallery","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9827b0d67fe20e61d70ea51e1e426fa5a4","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ce25403f2e31ba3d19062269561c847e","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/DKPhotoGallery","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"DKPhotoGallery","INFOPLIST_FILE":"Target Support Files/DKPhotoGallery/ResourceBundle-DKPhotoGallery-DKPhotoGallery-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"DKPhotoGallery","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98df96ed2e51e7449182a642516de846e8","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ce25403f2e31ba3d19062269561c847e","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/DKPhotoGallery","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"DKPhotoGallery","INFOPLIST_FILE":"Target Support Files/DKPhotoGallery/ResourceBundle-DKPhotoGallery-DKPhotoGallery-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"DKPhotoGallery","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9876ab7af6c01b94d92d8635675e0a1e9f","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98d4cceebc8223c203bda3ae043d4f5a2a","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98e3bb8fa9ad529b16922d74183d3e157e","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d290f95eda1d47523b5bfb4841891302","guid":"bfdfe7dc352907fc980b868725387e9877a16b4dcabff5ab7984c585d54c83cf"},{"fileReference":"bfdfe7dc352907fc980b868725387e983d8bce4e570c9338d5f5931c9ba86e89","guid":"bfdfe7dc352907fc980b868725387e9834bf0ed335b3589d93255c918a288123"},{"fileReference":"bfdfe7dc352907fc980b868725387e98a7725f51944b5ffc23b0346377fa1496","guid":"bfdfe7dc352907fc980b868725387e982d22b6c46d36583f0dd323e5502aa8d8"},{"fileReference":"bfdfe7dc352907fc980b868725387e9816be827b3d572d08a67dd5dec7a269db","guid":"bfdfe7dc352907fc980b868725387e98bfe1f8f8d6fb8ce49b0e510f209ce544"},{"fileReference":"bfdfe7dc352907fc980b868725387e98779af039b1704e42a85a53a76916a9e7","guid":"bfdfe7dc352907fc980b868725387e9844db95573d52e5489d14e78135f3f83b"}],"guid":"bfdfe7dc352907fc980b868725387e98dfeee6176dd4d9244fcd403266fdb0fe","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98d3f65728b12dd217475d1283ee417937","name":"DKPhotoGallery-DKPhotoGallery","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9845db4417944723d8b91f4a6c67e94d3c","name":"DKPhotoGallery.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98b02e34bf30053adbd95f2df89ceb6980","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/image_picker_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"image_picker_ios","INFOPLIST_FILE":"Target Support Files/image_picker_ios/ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"image_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e980e74c599f81be6f51219d3e76fff560f","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9802d37552ee35230789525435ce1d185e","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/image_picker_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"image_picker_ios","INFOPLIST_FILE":"Target Support Files/image_picker_ios/ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"image_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98f594875355b0109ba3661824011fa560","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9802d37552ee35230789525435ce1d185e","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/image_picker_ios","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"image_picker_ios","INFOPLIST_FILE":"Target Support Files/image_picker_ios/ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"image_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e988954c2ab32b8061670d7f0c9acf42716","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98a740d66d499d64a7ef449f54d5a207d1","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e980d40062b93aac519c10ddca67d541c47","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989ffd990970f195ec0fc35388981bd27e","guid":"bfdfe7dc352907fc980b868725387e98941bb438e2ce43c8015fb5cb21d95876"}],"guid":"bfdfe7dc352907fc980b868725387e983700146dfed5a2f1f7852252478cbd41","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98082dc85da1fc941e5234c7cc1f11b27d","name":"image_picker_ios-image_picker_ios_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98cba567c8a049008de84f093e54e3191c","name":"image_picker_ios_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c4b3f901f783f87404585b53fbe73d83","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SDWebImage","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"SDWebImage","INFOPLIST_FILE":"Target Support Files/SDWebImage/ResourceBundle-SDWebImage-SDWebImage-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"SDWebImage","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98d16fd49f72ef628cda2f9600716a0e19","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e981b703fcabd23454c5e4a540fb7bd3bff","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SDWebImage","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"SDWebImage","INFOPLIST_FILE":"Target Support Files/SDWebImage/ResourceBundle-SDWebImage-SDWebImage-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"SDWebImage","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9884cd3653c7bd548396122d2e6fa40603","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e981b703fcabd23454c5e4a540fb7bd3bff","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SDWebImage","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"SDWebImage","INFOPLIST_FILE":"Target Support Files/SDWebImage/ResourceBundle-SDWebImage-SDWebImage-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"SDWebImage","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98bcea497f976315de9514ba2ba60be88a","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e986a94baaf1a74b5ddccfd58051c18594c","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e988765330156fb2b1afdd444ae816c2c38","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e984d55a8610fe1c60f6bf809c2766925c4","guid":"bfdfe7dc352907fc980b868725387e98fbe4bda3c50532ddd96a2f19707793b9"}],"guid":"bfdfe7dc352907fc980b868725387e989477d9a49c5d2345a6ddd9ae61f2de6c","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9826e2628dc041aabe2d77e75ccb1dc95b","name":"SDWebImage-SDWebImage","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986798c379d76b6055dc2d719d4bd63a69","name":"SDWebImage.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9892664ed2fccbc2dcf887a88fa8db78c5","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989e2c69ee3fa07967191e9ea90584fcc7","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e986b02bf13c98fc749b18b9f7571f81941","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9812d0cfe09638a29fdee36bcc4ae60b5f","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e986b02bf13c98fc749b18b9f7571f81941","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989814c574dba090de4779eefb20a7e305","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e980667bfb91f71a4cc0820e9dc7084b81b","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98712ccf80cc6b994c2efd367e8aba88e1","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98c7f646795b6b2f40f13308ac061bf3eb","guid":"bfdfe7dc352907fc980b868725387e98298935b0809b0783469ddb570c766123"}],"guid":"bfdfe7dc352907fc980b868725387e98e01785208e008b1f7274dc7ec968aa64","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98e0be3b0d5ad56f1985578b1f97431765","name":"shared_preferences_foundation-shared_preferences_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98ad625504a4c1e61077bbfd33bd1d1785","name":"shared_preferences_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9840af901554a0b46373640fbc5c6c7dfb","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/file_picker","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"file_picker","INFOPLIST_FILE":"Target Support Files/file_picker/ResourceBundle-file_picker_ios_privacy-file_picker-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"file_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989cad7c6d22d8c37f977d95171fc16497","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e980d5a370773f8535b798fccfe62daed8b","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/file_picker","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"file_picker","INFOPLIST_FILE":"Target Support Files/file_picker/ResourceBundle-file_picker_ios_privacy-file_picker-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"file_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98465f876e14f9756787564170ea2583fc","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e980d5a370773f8535b798fccfe62daed8b","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/file_picker","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"file_picker","INFOPLIST_FILE":"Target Support Files/file_picker/ResourceBundle-file_picker_ios_privacy-file_picker-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"file_picker_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98771542af12ca1dd2f9a64664358bfe87","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9879927a51648a49d0d47af5cd9daa3a41","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e989a3c262f82281c6d58b03f8f98b079b2","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98cf65933b4a63d330f803e2e112507b15","guid":"bfdfe7dc352907fc980b868725387e981d9e89a298c2524cd7a572e4fab5e9d2"}],"guid":"bfdfe7dc352907fc980b868725387e98a61ce9f0bf5c1bc1a13882e5104d4e35","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e985452a642045cac0ef7c37f93da2d994e","name":"file_picker-file_picker_ios_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e985ae769d0b989789f9e90cfb215ac5a2e","name":"file_picker_ios_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -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"]}

View File

@ -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
}

View File

@ -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'),
),
),
],
),
);
}
}

View File

@ -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'),
),
],
);
}
}

View File

@ -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 Normal file
View File

@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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,
],
),
// autres propriétés si besoin
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,
],
),
),
),
),
);
}
}

View File

@ -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
}

View File

@ -0,0 +1,408 @@
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.produitImei != null) ...[
const SizedBox(height: 2),
Text(
'IMEI: ${detail.produitImei}',
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
fontStyle: FontStyle.italic,
),
)
],
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,
),
),
],
),
],
),
),
],
);
},
);
}
}

View File

@ -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),
),
],
),
);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}
}

View File

@ -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});
}

View File

@ -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),
],
),
),
),
);
}
}

View File

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

View File

@ -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,
),
],
);
}
}

View File

@ -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();
}
}

View File

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

View File

@ -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;
}

View File

@ -1,4 +1,4 @@
// Models/client.dart
// Models/client.dart - Version corrigée pour MySQL
class Client {
final int? id;
final String nom;
@ -33,31 +33,48 @@ 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
}
enum StatutCommande { enAttente, confirmee, annulee }
class Commande {
final int? id;
@ -67,25 +84,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,62 +140,54 @@ 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;
final String? produitImei; // NOUVEAU : IMEI du produit, si applicable
DetailCommande({
this.id,
@ -159,11 +196,205 @@ 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,
this.produitImei,
});
// 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,
String? produitImei,
}) {
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,
produitImei: produitImei,
);
}
// 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,
String? produitImei,
}) {
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,
produitImei: produitImei,
);
}
// 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,
produitImei: produitImei,
);
}
// 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,
produitImei: produitImei,
);
}
// 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,
produitImei: produitImei,
);
}
// 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,
produitImei: produitImei,
);
}
// Getters utiles
bool get aRemise => remiseType != null && montantRemise > 0 && !estCadeau;
bool get aimei => produitImei != null;
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 +403,41 @@ 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?,
produitImei: map['produitImei'] as String?,
);
}
}

64
lib/Models/Remise.dart Normal file
View File

@ -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;
}
}

View File

@ -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;
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 {
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 ?? '',
'image': image,
'category': category,
'stock': stock ?? 0,
'description': description ?? '',
'qrCode': qrCode ?? '',
'reference': reference ?? '',
'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;
}
factory Product.fromMap(Map<String, dynamic> map) {
// 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('\\'));
}
// 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,
);
}
}

View File

@ -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 ?? '';
}

View File

View File

@ -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 Normal file
View File

@ -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;

View File

@ -1,728 +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'},
{'name': 'Gérer les pointages', 'route': '/pointage'},
];
for (var menu in menusToAdd) {
final existing = await db.query(
'menu',
where: 'route = ?',
whereArgs: [menu['route']],
);
if (existing.isEmpty) {
await db.insert('menu', menu);
print("Menu ajouté: ${menu['name']}");
}
}
}
Future<void> insertDefaultRoles() async {
final db = await database;
final existingRoles = await db.query('roles');
if (existingRoles.isEmpty) {
int superAdminRoleId =
await db.insert('roles', {'designation': 'Super Admin'});
int adminRoleId = await db.insert('roles', {'designation': 'Admin'});
int userRoleId = await db.insert('roles', {'designation': 'User'});
final permissions = await db.query('permissions');
final menus = await db.query('menu');
// Assigner toutes les permissions à tous les menus pour le Super Admin
for (var menu in menus) {
for (var permission in permissions) {
await db.insert(
'role_menu_permissions',
{
'role_id': superAdminRoleId,
'menu_id': menu['id'],
'permission_id': permission['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
}
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId);
print("Rôles par défaut créés et permissions assignées");
} else {
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes
await _updateExistingRolePermissions(db);
}
}
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus
Future<void> _assignBasicPermissionsToRoles(
Database db, int adminRoleId, int userRoleId) async {
final viewPermission =
await db.query('permissions', where: 'name = ?', whereArgs: ['view']);
final createPermission =
await db.query('permissions', where: 'name = ?', whereArgs: ['create']);
final updatePermission =
await db.query('permissions', where: 'name = ?', whereArgs: ['update']);
final managePermission =
await db.query('permissions', where: 'name = ?', whereArgs: ['manage']);
// Récupérer les IDs des nouveaux menus
final nouvelleCommandeMenu = await db
.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']);
final gererCommandesMenu = await db
.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']);
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) {
// Admin peut créer de nouvelles commandes
await db.insert(
'role_menu_permissions',
{
'role_id': adminRoleId,
'menu_id': nouvelleCommandeMenu.first['id'],
'permission_id': createPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore);
// User peut aussi créer de nouvelles commandes
await db.insert(
'role_menu_permissions',
{
'role_id': userRoleId,
'menu_id': nouvelleCommandeMenu.first['id'],
'permission_id': createPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) {
// Admin peut gérer les commandes
await db.insert(
'role_menu_permissions',
{
'role_id': adminRoleId,
'menu_id': gererCommandesMenu.first['id'],
'permission_id': managePermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) {
// User peut voir les commandes
await db.insert(
'role_menu_permissions',
{
'role_id': userRoleId,
'menu_id': gererCommandesMenu.first['id'],
'permission_id': viewPermission.first['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
}
Future<void> _updateExistingRolePermissions(Database db) async {
final superAdminRole = await db
.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
if (superAdminRole.isNotEmpty) {
final superAdminRoleId = superAdminRole.first['id'] as int;
final permissions = await db.query('permissions');
final menus = await db.query('menu');
// Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus
for (var menu in menus) {
for (var permission in permissions) {
final existingPermission = await db.query(
'role_menu_permissions',
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
whereArgs: [superAdminRoleId, menu['id'], permission['id']],
);
if (existingPermission.isEmpty) {
await db.insert(
'role_menu_permissions',
{
'role_id': superAdminRoleId,
'menu_id': menu['id'],
'permission_id': permission['id'],
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
}
}
// Assigner les permissions de base aux autres rôles pour les nouveaux menus
final adminRole = await db
.query('roles', where: 'designation = ?', whereArgs: ['Admin']);
final userRole = await db
.query('roles', where: 'designation = ?', whereArgs: ['User']);
if (adminRole.isNotEmpty && userRole.isNotEmpty) {
await _assignBasicPermissionsToRoles(
db, adminRole.first['id'] as int, userRole.first['id'] as int);
}
print("Permissions mises à jour pour tous les rôles");
}
}
Future<void> insertDefaultSuperAdmin() async {
final db = await database;
final existingSuperAdmin = await db.rawQuery('''
SELECT u.* FROM users u
INNER JOIN roles r ON u.role_id = r.id
WHERE r.designation = 'Super Admin'
''');
if (existingSuperAdmin.isEmpty) {
final superAdminRole = await db
.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
if (superAdminRole.isNotEmpty) {
final superAdminRoleId = superAdminRole.first['id'] as int;
await db.insert('users', {
'name': 'Super',
'lastname': 'Admin',
'email': 'superadmin@youmazgestion.com',
'password': 'admin123',
'username': 'superadmin',
'role_id': superAdminRoleId,
});
print("Super Admin créé avec succès !");
print("Username: superadmin");
print("Password: admin123");
print(
"ATTENTION: Changez ce mot de passe après la première connexion !");
}
} else {
print("Super Admin existe déjà");
}
}
Future<int> createUser(Users user) async {
final db = await database;
return await db.insert('users', user.toMap());
}
Future<int> deleteUser(int id) async {
final db = await database;
return await db.delete('users', where: 'id = ?', whereArgs: [id]);
}
Future<int> updateUser(Users user) async {
final db = await database;
return await db
.update('users', user.toMap(), where: 'id = ?', whereArgs: [user.id]);
}
Future<int> getUserCount() async {
final db = await database;
List<Map<String, dynamic>> result =
await db.rawQuery('SELECT COUNT(*) as count FROM users');
return result.first['count'] as int;
}
Future<bool> verifyUser(String username, String password) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.id
FROM users
WHERE users.username = ? AND users.password = ?
''', [username, password]);
return result.isNotEmpty;
}
Future<Users> getUser(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.*, roles.designation as role_name
FROM users
INNER JOIN roles ON users.role_id = roles.id
WHERE users.username = ?
''', [username]);
if (result.isNotEmpty) {
return Users.fromMap(result.first);
} else {
throw Exception('User not found');
}
}
Future<Map<String, dynamic>?> getUserCredentials(
String username, String password) async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id
FROM users
INNER JOIN roles ON users.role_id = roles.id
WHERE username = ? AND password = ?
''', [username, password]);
if (result.isNotEmpty) {
return {
'id': result.first['id'],
'username': result.first['username'] as String,
'role': result.first['role_name'] as String,
'role_id': result.first['role_id'],
};
} else {
return null;
}
}
Future<List<Users>> getAllUsers() async {
final db = await database;
final result = await db.rawQuery('''
SELECT users.*, roles.designation as role_name
FROM users
INNER JOIN roles ON users.role_id = roles.id
ORDER BY users.id ASC
''');
return result.map((json) => Users.fromMap(json)).toList();
}
Future<int> createRole(Role role) async {
final db = await database;
return await db.insert('roles', role.toMap());
}
Future<List<Role>> getRoles() async {
final db = await database;
final maps = await db.query('roles', orderBy: 'designation ASC');
return List.generate(maps.length, (i) => Role.fromMap(maps[i]));
}
Future<int> updateRole(Role role) async {
final db = await database;
return await db.update(
'roles',
role.toMap(),
where: 'id = ?',
whereArgs: [role.id],
);
}
Future<int> deleteRole(int? id) async {
final db = await database;
return await db.delete(
'roles',
where: 'id = ?',
whereArgs: [id],
);
}
Future<List<Permission>> getAllPermissions() async {
final db = await database;
final result = await db.query('permissions', orderBy: 'name ASC');
return result.map((e) => Permission.fromMap(e)).toList();
}
Future<List<Permission>> getPermissionsForRole(int roleId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT p.id, p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?
ORDER BY p.name ASC
''', [roleId]);
return result.map((map) => Permission.fromMap(map)).toList();
}
Future<List<Permission>> getPermissionsForUser(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT DISTINCT p.id, p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN roles r ON rp.role_id = r.id
JOIN users u ON u.role_id = r.id
WHERE u.username = ?
ORDER BY p.name ASC
''', [username]);
return result.map((map) => Permission.fromMap(map)).toList();
}
Future<void> assignPermission(int roleId, int permissionId) async {
final db = await database;
await db.insert(
'role_permissions',
{
'role_id': roleId,
'permission_id': permissionId,
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removePermission(int roleId, int permissionId) async {
final db = await database;
await db.delete(
'role_permissions',
where: 'role_id = ? AND permission_id = ?',
whereArgs: [roleId, permissionId],
);
}
Future<void> assignMenuPermission(int menuId, int permissionId) async {
final db = await database;
await db.insert(
'menu_permissions',
{
'menu_id': menuId,
'permission_id': permissionId,
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removeMenuPermission(int menuId, int permissionId) async {
final db = await database;
await db.delete(
'menu_permissions',
where: 'menu_id = ? AND permission_id = ?',
whereArgs: [menuId, permissionId],
);
}
Future<bool> isSuperAdmin(String username) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(*) as count
FROM users u
INNER JOIN roles r ON u.role_id = r.id
WHERE u.username = ? AND r.designation = 'Super Admin'
''', [username]);
return (result.first['count'] as int) > 0;
}
Future<void> changePassword(
String username, String oldPassword, String newPassword) async {
final db = await database;
final isValidOldPassword = await verifyUser(username, oldPassword);
if (!isValidOldPassword) {
throw Exception('Ancien mot de passe incorrect');
}
await db.update(
'users',
{'password': newPassword},
where: 'username = ?',
whereArgs: [username],
);
}
Future<bool> hasPermission(
String username, String permissionName, String menuRoute) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(*) as count
FROM permissions p
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id
JOIN roles r ON rmp.role_id = r.id
JOIN users u ON u.role_id = r.id
JOIN menu m ON m.route = ?
WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id
''', [menuRoute, username, permissionName]);
return (result.first['count'] as int) > 0;
}
Future<void> close() async {
if (_database.isOpen) {
await _database.close();
}
}
Future<void> printDatabaseInfo() async {
final db = await database;
print("=== INFORMATIONS DE LA BASE DE DONNÉES ===");
final userCount = await getUserCount();
print("Nombre d'utilisateurs: $userCount");
final users = await getAllUsers();
print("Utilisateurs:");
for (var user in users) {
print(" - ${user.username} (${user.name} ) - Email: ${user.email}");
}
final roles = await getRoles();
print("Rôles:");
for (var role in roles) {
print(" - ${role.designation} (ID: ${role.id})");
}
final permissions = await getAllPermissions();
print("Permissions:");
for (var permission in permissions) {
print(" - ${permission.name} (ID: ${permission.id})");
}
print("=========================================");
}
Future<List<Permission>> getPermissionsForRoleAndMenu(
int roleId, int menuId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT p.id, p.name
FROM permissions p
JOIN role_menu_permissions rmp ON p.id = rmp.permission_id
WHERE rmp.role_id = ? AND rmp.menu_id = ?
ORDER BY p.name ASC
''', [roleId, menuId]);
return result.map((map) => Permission.fromMap(map)).toList();
}
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue
Future<void> deleteDatabaseFile() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'app_database.db');
final file = File(path);
if (await file.exists()) {
await file.delete();
print("Base de données utilisateur supprimée");
}
}
Future<void> assignRoleMenuPermission(
int roleId, int menuId, int permissionId) async {
final db = await database;
await db.insert(
'role_menu_permissions',
{
'role_id': roleId,
'menu_id': menuId,
'permission_id': permissionId,
},
conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> removeRoleMenuPermission(
int roleId, int menuId, int permissionId) async {
final db = await database;
await db.delete(
'role_menu_permissions',
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
whereArgs: [roleId, menuId, permissionId],
);
}
}

View File

@ -1,60 +0,0 @@
import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import '../Models/pointage_model.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
Database? _db;
Future<Database> get database async {
if (_db != null) return _db!;
_db = await _initDatabase();
return _db!;
}
Future<Database> _initDatabase() async {
String databasesPath = await getDatabasesPath();
String dbPath = join(databasesPath, 'pointage.db');
return await openDatabase(dbPath, version: 1, onCreate: _onCreate);
}
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE pointages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userName TEXT NOT NULL,
date TEXT NOT NULL,
heureArrivee TEXT NOT NULL,
heureDepart TEXT NOT NULL
)
''');
}
Future<int> insertPointage(Pointage pointage) async {
final db = await database;
return await db.insert('pointages', pointage.toMap());
}
Future<List<Pointage>> getPointages() async {
final db = await database;
final pointages = await db.query('pointages');
return pointages.map((pointage) => Pointage.fromMap(pointage)).toList();
}
Future<int> updatePointage(Pointage pointage) async {
final db = await database;
return await db.update('pointages', pointage.toMap(),
where: 'id = ?', whereArgs: [pointage.id]);
}
Future<int> deletePointage(int id) async {
final db = await database;
return await db.delete('pointages', where: 'id = ?', whereArgs: [id]);
}
}

View File

@ -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 Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2524
lib/Views/Dashboard.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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,
),
),
),
],
),
],
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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,8 +29,14 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
}
Future<void> _initData() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
final menuList = await db.getAllMenus(); // Utilise la nouvelle méthode
Map<int, Map<String, bool>> tempMenuPermissionsMap = {};
@ -46,11 +55,20 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
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 {
try {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
@ -64,61 +82,226 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
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,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: "Permissions - ${widget.role.designation}",
// showBackButton: true,
),
body: Padding(
void _toggleAllPermissions(int menuId, bool enabled) {
for (var permission in permissions) {
_onPermissionToggle(menuId, permission.name, enabled);
}
}
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: [
Row(
children: [
Icon(Icons.analytics, color: Colors.blue.shade600),
const SizedBox(width: 8),
Text(
'Gestion des permissions pour le rôle: ${widget.role.designation}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
'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: 20),
if (permissions.isNotEmpty && menus.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: menus.length,
itemBuilder: (context, index) {
final menu = menus[index];
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: 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: 15),
margin: const EdgeInsets.only(bottom: 16),
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
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(
menuName,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
menuRoute,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
const SizedBox(height: 8),
),
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,
),
),
),
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: 10,
runSpacing: 10,
spacing: 8,
runSpacing: 8,
children: permissions.map((perm) {
final isChecked = menuPermissionsMap[menuId]?[perm.name] ?? false;
return FilterChip(
return CustomFilterChip(
label: perm.name,
selected: isChecked,
onSelected: (bool value) {
@ -130,48 +313,275 @@ class _RolePermissionsPageState extends State<RolePermissionsPage> {
],
),
),
],
),
);
},
}
@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'),
),
],
),
)
else
const Expanded(
child: Center(
child: CircularProgressIndicator(),
: 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,
);
}
}

View File

@ -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),
),
),
),
),
],
),
],
),
),
);
}
}

View File

@ -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,
),

File diff suppressed because it is too large Load Diff

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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();
} catch (e) {
print('Erreur de mise à jour: $e');
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('Échec', 'Une erreur est survenue lors de la mise à jour.');
_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');
if (mounted) {
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,51 +297,145 @@ 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),
_buildDropdown(),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 48,
_buildRoleDropdown(),
const SizedBox(height: 12),
_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'),
),
),
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 CircularProgressIndicator(color: Colors.white)
: const Text('Mettre à jour', style: TextStyle(color: Colors.white, fontSize: 16)),
? 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(),
],
),
),
);
}
}

View File

@ -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(),
@ -84,7 +85,9 @@ class GestionProduit extends StatelessWidget {
}
Widget _buildProductImage(String? imagePath) {
if (imagePath != null && imagePath.isNotEmpty && File(imagePath).existsSync()) {
if (imagePath != null &&
imagePath.isNotEmpty &&
File(imagePath).existsSync()) {
return CircleAvatar(
backgroundImage: FileImage(File(imagePath)),
);

View File

@ -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,9 +29,12 @@ class _HandleUserRoleState extends State<HandleUserRole> {
}
Future<void> _initData() async {
try {
final roleList = await db.getRoles();
final perms = await db.getAllPermissions();
final menuList = await db.database.then((db) => db.query('menu'));
// Récupération mise à jour des menus avec gestion d'erreur
final menuList = await db.getAllMenus();
Map<int, Map<int, Map<String, bool>>> tempRoleMenuPermissionsMap = {};
@ -55,18 +59,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
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();
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 {
try {
final perm = permissions.firstWhere((p) => p.name == permission);
if (enabled) {
@ -78,12 +130,76 @@ class _HandleUserRoleState extends State<HandleUserRole> {
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,
),
);
}
}
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;
}
// 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,22 +276,64 @@ 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,
),
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: menus.map((menu) {
final menuId = menu['id'] as int;
return Column(
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),
),
child: Row(
children: [
Icon(Icons.menu, color: Colors.blue.shade700),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menu['name'],
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
menuName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue.shade700,
),
DataTable(
),
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(
@ -160,17 +342,49 @@ class _HandleUserRoleState extends State<HandleUserRole> {
),
),
...permissions.map((perm) => DataColumn(
label: Text(
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),
),
),
],
rows: roles.map((role) {
final roleId = role.id!;
return DataRow(
cells: [
DataCell(Text(role.designation)),
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(
@ -179,26 +393,66 @@ class _HandleUserRoleState extends State<HandleUserRole> {
onChanged: (bool? value) {
_onPermissionToggle(roleId, menuId, perm.name, value ?? false);
},
activeColor: Colors.green,
),
);
}).toList(),
],
);
}).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();
}
}

View File

@ -2,9 +2,11 @@ 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';
import 'package:youmazgestion/controller/userController.dart';
class GestionStockPage extends StatefulWidget {
const GestionStockPage({super.key});
@ -14,10 +16,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;
@ -42,13 +45,19 @@ class _GestionStockPageState extends State<GestionStockPage> {
setState(() {
_filteredProducts = _products.where((product) {
final matchesSearch = product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false);
final matchesCategory = _selectedCategory == null ||
product.category == _selectedCategory;
(product.reference?.toLowerCase().contains(query) ?? false) ||
((product.imei ?? '').toLowerCase().contains(query));
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 +88,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 +120,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
),
const SizedBox(height: 12),
// Filtres
// Filtres - Première ligne
Row(
children: [
// Filtre par catégorie
@ -155,6 +164,55 @@ 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(
@ -276,7 +334,8 @@ class _GestionStockPageState extends State<GestionStockPage> {
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
Widget _buildStatCard(
String title, String value, IconData icon, Color color) {
return Column(
children: [
Icon(icon, color: color),
@ -298,6 +357,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
}
Widget _buildProductCard(Product product) {
final UserController _userController = Get.find<UserController>();
final stock = product.stock ?? 0;
Color stockColor;
IconData stockIcon;
@ -329,8 +389,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(
@ -359,11 +419,14 @@ class _GestionStockPageState extends State<GestionStockPage> {
),
],
),
if (_userController.username == 'superadmin' ||
_userController.username == 'admin') ...[
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showAddStockDialog(product),
),
],
],
),
),
);
@ -385,13 +448,14 @@ class _GestionStockPageState extends State<GestionStockPage> {
children: [
if (isNew)
TextField(
decoration: const InputDecoration(labelText: 'Nom du produit'),
decoration:
const InputDecoration(labelText: 'Nom du produit'),
),
if (isNew)
const SizedBox(height: 12),
if (isNew) const SizedBox(height: 12),
TextField(
controller: controller,
decoration: const InputDecoration(labelText: 'Quantité en stock'),
decoration:
const InputDecoration(labelText: 'Quantité en stock'),
keyboardType: TextInputType.number,
),
],

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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),
),
],
),
),
],
],
),
),
);
}
}

View File

@ -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: () {

View File

@ -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);
}
Future<void> getUsersFromDatabase() async {
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
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');
}
}
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),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Nom: ${user.name} ${user.lastName}",
style: const TextStyle(fontWeight: FontWeight.w500)),
Text("Username: ${user.username}"),
Text("Email: ${user.email}"),
Text("Rôle: ${user.roleName ?? 'N/A'}"),
Text("Point de vente: ${_getPointDeVenteName(user.pointDeVenteId)}"),
],
),
),
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: [
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: 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),
appBar: CustomAppBar(title: 'Liste des utilisateurs'),
body: isLoading
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement des utilisateurs...'),
],
),
shadowColor: Colors.deepOrange,
borderOnForeground: true,
child: ListTile(
title: Text(
)
: 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,
),
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,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${user.name} ${user.lastName}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
subtitle: Column(
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),
Text("Username: ${user.username}"),
_buildInfoChip(Icons.badge, user.roleName ?? 'N/A'),
const SizedBox(height: 4),
Text("Privilège: ${user.role}"),
_buildInfoChip(Icons.store, pointDeVenteName),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
),
// Boutons d'actions
Column(
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"),
),
TextButton(
onPressed: () async {
await AppDatabase.instance
.deleteUser(user.id!);
Navigator.of(context).pop();
setState(() {
userList.removeAt(index);
});
},
child: const Text("Supprimer"),
),
],
);
},
);
},
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),
color: Colors.blue,
icon: const Icon(Icons.edit, size: 20),
color: Colors.green.shade600,
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));
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,
),
),
],
);
}
}

View File

@ -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,7 +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
print('Nombre d\'utilisateurs trouvés: $userCount');
} catch (error) {
print('Erreur lors de la vérification du nombre d\'utilisateurs: $error');
setState(() {
@ -51,17 +55,46 @@ class _LoginPageState extends State<LoginPage> {
super.dispose();
}
Future<void> saveUserData(Users user, String role, int userId) async {
// /// OPTIMISÉ: Sauvegarde avec préchargement des permissions
// Future<void> saveUserData(Users user, String role, int userId) async {
// try {
// userController.setUserWithCredentials(user, role, userId);
// 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 {
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');
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
}
}
/// OPTIMISÉ: Connexion avec préchargement parallèle
void _login() async {
if (_isLoading) return;
@ -70,8 +103,7 @@ class _LoginPageState extends State<LoginPage> {
if (username.isEmpty || password.isEmpty) {
setState(() {
_errorMessage =
'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
_isErrorVisible = true;
});
return;
@ -80,31 +112,87 @@ class _LoginPageState extends State<LoginPage> {
setState(() {
_isLoading = true;
_isErrorVisible = false;
_loadingMessage = 'Connexion...';
});
try {
print('🔐 Tentative de connexion pour: $username');
final dbInstance = AppDatabase.instance;
// Vérifier les identifiants
// 1. Vérification rapide de la base
setState(() {
_loadingMessage = 'Vérification...';
});
try {
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...';
});
bool isValidUser = await dbInstance.verifyUser(username, password);
if (isValidUser) {
Users user = await dbInstance.getUser(username);
Map<String, dynamic>? userCredentials =
await dbInstance.getUserCredentials(username, password);
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);
// 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()),
);
}
}
// Les permissions se chargeront en arrière-plan après la navigation
print('🚀 Navigation immédiate, permissions en arrière-plan');
} else {
throw Exception('Erreur lors de la récupération des credentials');
}
@ -120,7 +208,30 @@ class _LoginPageState extends State<LoginPage> {
_isErrorVisible = true;
});
} finally {
if (mounted) setState(() => _isLoading = false);
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');
}
}
@ -142,8 +253,7 @@ class _LoginPageState extends State<LoginPage> {
width: MediaQuery.of(context).size.width < 500
? double.infinity
: 400,
padding:
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
decoration: BoxDecoration(
color: cardColor.withOpacity(0.98),
borderRadius: BorderRadius.circular(30.0),
@ -159,6 +269,7 @@ class _LoginPageState extends State<LoginPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Center(
child: Column(
children: [
@ -192,6 +303,8 @@ class _LoginPageState extends State<LoginPage> {
),
),
const SizedBox(height: 24),
// Username Field
TextField(
controller: _usernameController,
enabled: !_isLoading,
@ -214,6 +327,8 @@ class _LoginPageState extends State<LoginPage> {
),
),
const SizedBox(height: 18.0),
// Password Field
TextField(
controller: _passwordController,
enabled: !_isLoading,
@ -236,19 +351,104 @@ class _LoginPageState extends State<LoginPage> {
),
onSubmitted: (_) => _login(),
),
if (_isErrorVisible) ...[
const SizedBox(height: 12.0),
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: 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(
_errorMessage,
style: const TextStyle(
color: Colors.redAccent,
fontSize: 15,
fontWeight: FontWeight.w600,
"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 SizedBox(height: 26.0),
// Login Button
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
@ -262,13 +462,27 @@ class _LoginPageState extends State<LoginPage> {
minimumSize: const Size(double.infinity, 52),
),
child: _isLoading
? const SizedBox(
height: 24,
width: 24,
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
strokeWidth: 2,
),
),
const SizedBox(width: 12),
Text(
'Connexion...',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
)
: const Text(
'Se connecter',
@ -280,16 +494,23 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
// Option debug, à enlever en prod
if (_isErrorVisible) ...[
// Debug Button (à enlever en production)
if (_isErrorVisible && !_isLoading) ...[
const SizedBox(height: 8),
TextButton(
onPressed: () async {
try {
final count =
await AppDatabase.instance.getUserCount();
final count = await AppDatabase.instance.getUserCount();
final stats = _cacheService.getCacheStats();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$count utilisateurs trouvés')),
content: Text(
'BDD: $count utilisateurs\n'
'Cache: ${stats['users_cached']} utilisateurs en cache',
),
duration: const Duration(seconds: 3),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
@ -297,7 +518,13 @@ class _LoginPageState extends State<LoginPage> {
);
}
},
child: const Text('Debug: Vérifier BDD'),
child: Text(
'Debug: Vérifier BDD & Cache',
style: TextStyle(
color: primaryColor.withOpacity(0.6),
fontSize: 12,
),
),
),
],
],

Some files were not shown because too many files have changed in this diff Show More