Compare commits
44 Commits
master
...
28062025_0
| Author | SHA1 | Date | |
|---|---|---|---|
| 58154af680 | |||
| 865410ae93 | |||
| 13554ee49c | |||
| c757bdb701 | |||
| 4b4d9637fd | |||
| 2eeaeaa4c4 | |||
| f509af1716 | |||
| ab97716dd2 | |||
| c5ef3ca0cb | |||
| f41fc922a6 | |||
| d043f6f20b | |||
| 13296529c3 | |||
| 99d570bd3a | |||
| 5ad019d35e | |||
| a52749c415 | |||
| 3e6a81c2c3 | |||
| 2764111fa4 | |||
| b415b9f501 | |||
| ab398bddc6 | |||
| 01e9cabeba | |||
| 1f2a255719 | |||
| 3fcc50195c | |||
| 55822cd52e | |||
| 6b83a1ab5e | |||
|
|
14ce881a3c | ||
|
|
6bc8373cad | ||
|
|
551831df74 | ||
|
|
cb59287bfd | ||
|
|
55e896775d | ||
| 46826006c2 | |||
| e02c4c8bef | |||
|
|
332ed228ae | ||
|
|
2af3b01d92 | ||
| be8c169ad1 | |||
|
|
48ae916f02 | ||
|
|
c0bbb0da2b | ||
|
|
595b38e9fb | ||
|
|
525b09c81f | ||
|
|
b5a11aa3c9 | ||
|
|
831cce13da | ||
|
|
c8fedd08e5 | ||
|
|
9eafda610f | ||
|
|
2bef06a2fe | ||
|
|
57ea91b3d7 |
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 17 KiB |
BIN
assets/NotoEmoji-Regular.ttf
Normal file
BIN
assets/Orange_money.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/airtel_money.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/fa-solid-900.ttf
Normal file
BIN
assets/fonts/Roboto-Italic.ttf
Normal file
BIN
assets/mvola.jpg
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
4
flutter_launcher_icons.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
android: true
|
||||
ios: true
|
||||
macos: true
|
||||
image_path: assets/youmaz2.png
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -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"}
|
||||
@ -0,0 +1 @@
|
||||
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/rabarisonmimistephanethannio/Documents/DEV/guycom_finale/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=7e9f9a517e1b730b3eb5b9aa5a52f2df_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
|
||||
430
lib/Components/AddClient.dart
Normal 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
|
||||
}
|
||||
471
lib/Components/AddClientForm.dart
Normal 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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
176
lib/Components/DiscountDialog.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
349
lib/Components/GiftaselectedButton.dart
Normal 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
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
lib/Components/colors.dart
Normal 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
|
||||
}
|
||||
408
lib/Components/commandManagementComponents/CommandDetails.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
220
lib/Components/commandManagementComponents/CommandeActions.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
190
lib/Components/commandManagementComponents/DiscountDialog.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:youmazgestion/Models/client.dart';
|
||||
import 'package:youmazgestion/Models/produit.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
|
||||
|
||||
// Dialog pour sélectionner un cadeau
|
||||
class GiftSelectionDialog extends StatefulWidget {
|
||||
final Commande commande;
|
||||
|
||||
const GiftSelectionDialog({super.key, required this.commande});
|
||||
|
||||
@override
|
||||
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
|
||||
}
|
||||
|
||||
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
|
||||
List<Product> _products = [];
|
||||
List<Product> _filteredProducts = [];
|
||||
final _searchController = TextEditingController();
|
||||
Product? _selectedProduct;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
_searchController.addListener(_filterProducts);
|
||||
}
|
||||
|
||||
Future<void> _loadProducts() async {
|
||||
final products = await AppDatabase.instance.getProducts();
|
||||
setState(() {
|
||||
_products = products.where((p) => p.stock > 0).toList();
|
||||
_filteredProducts = _products;
|
||||
});
|
||||
}
|
||||
|
||||
void _filterProducts() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
_filteredProducts = _products.where((product) {
|
||||
return product.name.toLowerCase().contains(query) ||
|
||||
(product.reference?.toLowerCase().contains(query) ?? false) ||
|
||||
(product.category.toLowerCase().contains(query));
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Sélectionner un cadeau'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Rechercher un produit',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = _filteredProducts[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: product.image != null
|
||||
? Image.network(
|
||||
product.image!,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Icon(Icons.image_not_supported),
|
||||
)
|
||||
: const Icon(Icons.phone_android),
|
||||
title: Text(product.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Catégorie: ${product.category}'),
|
||||
Text('Stock: ${product.stock}'),
|
||||
if (product.reference != null)
|
||||
Text('Réf: ${product.reference}'),
|
||||
],
|
||||
),
|
||||
trailing: Radio<Product>(
|
||||
value: product,
|
||||
groupValue: _selectedProduct,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedProduct = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedProduct = product;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _selectedProduct != null
|
||||
? () => Navigator.pop(context, _selectedProduct)
|
||||
: null,
|
||||
child: const Text('Ajouter le cadeau'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
234
lib/Components/commandManagementComponents/PaswordRequired.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
enum PaymentType {
|
||||
cash,
|
||||
card,
|
||||
mvola,
|
||||
orange,
|
||||
airtel
|
||||
}
|
||||
412
lib/Components/newCommandComponents/CadeauDialog.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
332
lib/Components/newCommandComponents/RemiseDialog.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
7
lib/Components/paymentType.dart
Normal file
@ -0,0 +1,7 @@
|
||||
enum PaymentType {
|
||||
cash,
|
||||
card,
|
||||
mvola,
|
||||
orange,
|
||||
airtel
|
||||
}
|
||||
831
lib/Components/windows_qr_scanner.dart
Normal 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;
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
|
||||
// Models/detail_commande.dart
|
||||
// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
|
||||
enum RemiseType { pourcentage, montant }
|
||||
|
||||
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
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
// 1. Models/users.dart - Version corrigée
|
||||
class Users {
|
||||
int? id;
|
||||
String name;
|
||||
@ -6,7 +7,8 @@ class Users {
|
||||
String password;
|
||||
String username;
|
||||
int roleId;
|
||||
String? roleName; // Optionnel, rempli lors des requêtes avec JOIN
|
||||
String? roleName;
|
||||
int? pointDeVenteId;
|
||||
|
||||
Users({
|
||||
this.id,
|
||||
@ -17,38 +19,43 @@ class Users {
|
||||
required this.username,
|
||||
required this.roleId,
|
||||
this.roleName,
|
||||
this.pointDeVenteId,
|
||||
});
|
||||
|
||||
// ✅ CORRIGÉ: Méthode toMap() qui correspond exactement aux colonnes de la DB
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id, // ✅ Inclure l'ID pour les updates
|
||||
'name': name,
|
||||
'lastname': lastName,
|
||||
'lastname': lastName, // ✅ Correspond à la colonne DB
|
||||
'email': email,
|
||||
'password': password,
|
||||
'username': username,
|
||||
'role_id': roleId,
|
||||
'point_de_vente_id': pointDeVenteId,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMapWithId() {
|
||||
// ✅ Méthode pour créer un map sans l'ID (pour les insertions)
|
||||
Map<String, dynamic> toMapForInsert() {
|
||||
final map = toMap();
|
||||
if (id != null) map['id'] = id;
|
||||
map.remove('id');
|
||||
return map;
|
||||
}
|
||||
|
||||
factory Users.fromMap(Map<String, dynamic> map) {
|
||||
return Users(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
lastName: map['lastname'],
|
||||
email: map['email'],
|
||||
password: map['password'],
|
||||
username: map['username'],
|
||||
roleId: map['role_id'],
|
||||
roleName: map['role_name'], // Depuis les requêtes avec JOIN
|
||||
id: map['id'] as int?,
|
||||
name: map['name'] as String,
|
||||
lastName: map['lastname'] as String, // ✅ Correspond à la colonne DB
|
||||
email: map['email'] as String,
|
||||
password: map['password'] as String,
|
||||
username: map['username'] as String,
|
||||
roleId: map['role_id'] as int,
|
||||
roleName: map['role_name'] as String?, // Depuis les JOINs
|
||||
pointDeVenteId: map['point_de_vente_id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
// Getter pour la compatibilité avec l'ancien code
|
||||
String get role => roleName ?? '';
|
||||
}
|
||||
0
lib/Services/GestionStockDatabase.dart
Normal file
258
lib/Services/PermissionCacheService.dart
Normal 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
@ -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;
|
||||
@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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
3574
lib/Services/stock_managementDatabase.dart
Normal file
2524
lib/Views/Dashboard.dart
Normal file
847
lib/Views/DemandeTransfert.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
452
lib/Views/approbation_sorties_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
|
||||
979
lib/Views/demande_sortie_personnelle_page.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)),
|
||||
);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
1121
lib/Views/gestion_point_de_vente.dart
Normal file
356
lib/Views/historique_sorties_personnelles_page.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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: () {
|
||||
|
||||
@ -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,
|
||||
appBar: CustomAppBar(title: 'Liste des utilisateurs'),
|
||||
body: isLoading
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Chargement des utilisateurs...'),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
// Barre de recherche et filtres
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.grey.shade50,
|
||||
child: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par nom, username, email...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Filtre par point de vente
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.filter_list, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Point de vente:'),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int?>(
|
||||
value: selectedPointDeVenteFilter,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
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 = userList[index];
|
||||
return Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shadowColor: Colors.deepOrange,
|
||||
borderOnForeground: true,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
"${user.name} ${user.lastName}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text("Username: ${user.username}"),
|
||||
const SizedBox(height: 4),
|
||||
Text("Privilège: ${user.role}"),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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();
|
||||
Users user = filteredUserList[index];
|
||||
return _buildUserCard(user, index);
|
||||
},
|
||||
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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
color: Colors.blue,
|
||||
onPressed: () {
|
||||
// Action de modification
|
||||
// Vous pouvez naviguer vers la page de modification avec les détails de l'utilisateur
|
||||
// en utilisant Navigator.push ou showDialog, selon votre besoin
|
||||
Get.to(() => EditUserPage(user: user));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"@${user.username}",
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSuperAdmin)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'ADMIN',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade800,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations détaillées
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoChip(Icons.email, user.email),
|
||||
const SizedBox(height: 4),
|
||||
_buildInfoChip(Icons.badge, user.roleName ?? 'N/A'),
|
||||
const SizedBox(height: 4),
|
||||
_buildInfoChip(Icons.store, pointDeVenteName),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Boutons d'actions
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility, size: 20),
|
||||
color: Colors.blue.shade600,
|
||||
onPressed: () => _showUserDetails(user),
|
||||
tooltip: 'Voir les détails',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
color: Colors.green.shade600,
|
||||
onPressed: () {
|
||||
Get.to(() => EditUserPage(user: user))?.then((_) => _loadData());
|
||||
},
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isSuperAdmin ? Icons.lock : Icons.delete,
|
||||
size: 20,
|
||||
),
|
||||
color: isSuperAdmin ? Colors.grey : Colors.red.shade600,
|
||||
onPressed: isSuperAdmin
|
||||
? null
|
||||
: () => _deleteUser(user, index),
|
||||
tooltip: isSuperAdmin ? 'Protection Super Admin' : 'Supprimer',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoChip(IconData icon, String text) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 14, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,18 +55,47 @@ class _LoginPageState extends State<LoginPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> saveUserData(Users user, String role, int userId) async {
|
||||
try {
|
||||
userController.setUserWithCredentials(user, role, userId);
|
||||
print(
|
||||
'Utilisateur sauvegardé: ${user.username}, rôle: $role, id: $userId');
|
||||
} catch (error) {
|
||||
print('Erreur lors de la sauvegarde: $error');
|
||||
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
||||
}
|
||||
}
|
||||
// /// ✅ OPTIMISÉ: Sauvegarde avec préchargement des permissions
|
||||
// Future<void> saveUserData(Users user, String role, int userId) async {
|
||||
// try {
|
||||
// userController.setUserWithCredentials(user, role, userId);
|
||||
|
||||
void _login() async {
|
||||
// if (user.pointDeVenteId != null) {
|
||||
// await userController.loadPointDeVenteDesignation();
|
||||
// }
|
||||
|
||||
// print('✅ Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
|
||||
// } catch (error) {
|
||||
// print('❌ Erreur lors de la sauvegarde: $error');
|
||||
// throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
||||
// }
|
||||
// }
|
||||
|
||||
/// ✅ NOUVEAU: Préchargement des permissions en arrière-plan
|
||||
Future<void> _preloadUserPermissions(String username) async {
|
||||
try {
|
||||
setState(() {
|
||||
_loadingMessage = 'Préparation du menu...';
|
||||
});
|
||||
|
||||
// Lancer le préchargement en parallèle avec les autres tâches
|
||||
final permissionFuture = _cacheService.preloadUserData(username);
|
||||
|
||||
// Attendre maximum 2 secondes pour les permissions
|
||||
await Future.any([
|
||||
permissionFuture,
|
||||
Future.delayed(const Duration(seconds: 2))
|
||||
]);
|
||||
|
||||
print('✅ Permissions préparées (ou timeout)');
|
||||
} catch (e) {
|
||||
print('⚠️ Erreur préchargement permissions: $e');
|
||||
// Continuer même en cas d'erreur
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ OPTIMISÉ: Connexion avec préchargement parallèle
|
||||
void _login() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
final String username = _usernameController.text.trim();
|
||||
@ -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,9 +208,32 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||