diff --git a/assets/images/calendar_icon.svg b/assets/images/calendar_icon.svg new file mode 100644 index 00000000..bdf23bee --- /dev/null +++ b/assets/images/calendar_icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/device_note.svg b/assets/images/device_note.svg new file mode 100644 index 00000000..f0b94043 --- /dev/null +++ b/assets/images/device_note.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/empty_table.svg b/assets/images/empty_table.svg new file mode 100644 index 00000000..24ac359d --- /dev/null +++ b/assets/images/empty_table.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/time_icon.svg b/assets/images/time_icon.svg new file mode 100644 index 00000000..a8f06677 --- /dev/null +++ b/assets/images/time_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6094b4c6..dffe452f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 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 */; }; + E44A9405B1EB1B638DD05A58 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABF0EC746A2D686A0ED574F /* Pods_RunnerTests.framework */; }; + FF49F60EC38658783D8D66DA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AFAE479A87ECDEBD5D6EB30 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,12 +44,19 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 22428D486F110EE0B969469D /* 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 = ""; }; + 253C5EA6840355311DB030EA /* 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 = ""; }; + 2AFAE479A87ECDEBD5D6EB30 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2C0D722D2ED971BF672D18D5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 544621C7727C798253BAB2C8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7ABF0EC746A2D686A0ED574F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 877FDC97D8B87080E35B3EB7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,19 +64,52 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D3AD250AADBF93406007C9EB /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 759A57780A409ED209817654 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E44A9405B1EB1B638DD05A58 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FF49F60EC38658783D8D66DA /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1454C118FFCECEEDF59152D2 /* Pods */ = { + isa = PBXGroup; + children = ( + 253C5EA6840355311DB030EA /* Pods-Runner.debug.xcconfig */, + 22428D486F110EE0B969469D /* Pods-Runner.release.xcconfig */, + D3AD250AADBF93406007C9EB /* Pods-Runner.profile.xcconfig */, + 2C0D722D2ED971BF672D18D5 /* Pods-RunnerTests.debug.xcconfig */, + 877FDC97D8B87080E35B3EB7 /* Pods-RunnerTests.release.xcconfig */, + 544621C7727C798253BAB2C8 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 20A3C64D2B1CFED5A81C3251 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2AFAE479A87ECDEBD5D6EB30 /* Pods_Runner.framework */, + 7ABF0EC746A2D686A0ED574F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -94,6 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 1454C118FFCECEEDF59152D2 /* Pods */, + 20A3C64D2B1CFED5A81C3251 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + B9A66CAAF434B6A1BD8C4E09 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 759A57780A409ED209817654 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + C1C48B0232C0B26BFF405512 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 33590C9CD073D3D5EBA02CDE /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 33590C9CD073D3D5EBA02CDE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +318,50 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + B9A66CAAF434B6A1BD8C4E09 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C1C48B0232C0B26BFF405512 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -378,6 +487,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 2C0D722D2ED971BF672D18D5 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -395,6 +505,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 877FDC97D8B87080E35B3EB7 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -410,6 +521,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 544621C7727C798253BAB2C8 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/main.dart b/lib/main.dart index d52c6541..01911d05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/auth/view/login_page.dart'; +import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -23,7 +26,13 @@ class MyApp extends StatelessWidget { }); @override Widget build(BuildContext context) { - return MaterialApp( + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => HomeBloc()), + BlocProvider( + create: (context) => VisitorPasswordBloc(),) + ], + child: MaterialApp( debugShowCheckedModeBanner: false, // Hide debug banner scrollBehavior: const MaterialScrollBehavior().copyWith( dragDevices: { @@ -33,10 +42,10 @@ class MyApp extends StatelessWidget { PointerDeviceKind.unknown, }, ), + theme: ThemeData( textTheme: const TextTheme( - bodySmall: TextStyle( - fontSize: 13, color: ColorsManager.whiteColors, fontWeight: FontWeight.bold), + bodySmall: TextStyle(fontSize: 13, color: ColorsManager.whiteColors, fontWeight: FontWeight.bold), bodyMedium: TextStyle(color: Colors.black87, fontSize: 14), bodyLarge: TextStyle(fontSize: 16, color: Colors.white), headlineSmall: TextStyle(color: Colors.black87, fontSize: 18), @@ -47,10 +56,11 @@ class MyApp extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), // Set up color scheme useMaterial3: true, // Enable Material 3 ), - home: isLoggedIn == 'Success' ? const HomePage() : const LoginPage(), - ); + home:isLoggedIn == 'Success' ? const HomePage() : const LoginPage(), + )); } } diff --git a/lib/pages/access_management/bloc/access_bloc.dart b/lib/pages/access_management/bloc/access_bloc.dart new file mode 100644 index 00000000..47f865ef --- /dev/null +++ b/lib/pages/access_management/bloc/access_bloc.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; +import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/services/access_mang_api.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/const.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; + +class AccessBloc extends Bloc { + AccessBloc() : super((AccessInitial())) { + on(_onFetchTableData); + // on(selectFilterTap); + on(selectTime); + on(_filterData); + on(resetSearch); + on(onTabChanged); + } + String startTime = 'Start Date'; + String endTime = 'End Date'; + + int? effectiveTimeTimeStamp; + int? expirationTimeTimeStamp; + TextEditingController passwordName= TextEditingController(); + List filteredData = []; + List data=[]; + + Future _onFetchTableData( + FetchTableData event, Emitter emit) async { + try { + emit(AccessLoaded()); + data = await AccessMangApi().fetchVisitorPassword(); + filteredData= data; + updateTabsCount(); + emit(TableLoaded(data)); + } catch (e) { + emit(FailedState(e.toString())); + } + } + void updateTabsCount() { + int toBeEffectiveCount = data.where((item) => item.passwordStatus.value== 'To Be Effective').length; + int effectiveCount = data.where((item) => item.passwordStatus.value == 'Effective').length; + int expiredCount = data.where((item) => item.passwordStatus.value == 'Expired').length; + tabs[1] = 'To Be Effective ($toBeEffectiveCount)'; + tabs[2] = 'Effective ($effectiveCount)'; + tabs[3] = 'Expired ($expiredCount)'; + } + + + + int selectedIndex = 0; + final List tabs = [ + 'All', + 'To Be Effective (0)', + 'Effective (0)', + 'Expired' + ]; + + + Future selectFilterTap(TabChangedEvent event, Emitter emit) async { + try { + emit(AccessLoaded()); + selectedIndex= event.selectedIndex; + emit(AccessInitial()); + emit(TableLoaded(data)); + } catch (e) { + emit(FailedState( e.toString())); + return; + } + } + + + Future selectTime(SelectTime event, Emitter emit) async { + emit(AccessLoaded()); + + final DateTime? picked = await showDatePicker( + context: event.context, + initialDate: DateTime.now(), + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101), + ); + if (picked != null) { + final TimeOfDay? timePicked = await showTimePicker( + context: event.context, + initialTime: TimeOfDay.now(), + + builder: (context, child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + onSurface: Colors.black, + ), + buttonTheme: const ButtonThemeData( + colorScheme: ColorScheme.light( + primary: Colors.green, + ), + ), + ), + child: child!, + ); + }, + ); + if (timePicked != null) { + final selectedDateTime = DateTime( + picked.year, + picked.month, + picked.day, + timePicked.hour, + timePicked.minute, + ); + final selectedTimestamp = DateTime( + selectedDateTime.year, + selectedDateTime.month, + selectedDateTime.day, + selectedDateTime.hour, + selectedDateTime.minute, + ).millisecondsSinceEpoch ~/ 1000; // Divide by 1000 to remove milliseconds + if (event.isStart) { + if (expirationTimeTimeStamp != null && selectedTimestamp > expirationTimeTimeStamp!) { + CustomSnackBar.displaySnackBar('Effective Time cannot be later than Expiration Time.'); + } else { + startTime = selectedDateTime.toString().split('.').first; // Remove seconds and milliseconds + effectiveTimeTimeStamp = selectedTimestamp; + } + } else { + if (effectiveTimeTimeStamp != null && selectedTimestamp < effectiveTimeTimeStamp!) { + CustomSnackBar.displaySnackBar('Expiration Time cannot be earlier than Effective Time.'); + } else { + endTime = selectedDateTime.toString().split('.').first; // Remove seconds and milliseconds + expirationTimeTimeStamp = selectedTimestamp; + } + } + } + } + emit(ChangeTimeState()); + } + + + Future _filterData(FilterDataEvent event, Emitter emit) async { + emit(AccessLoaded()); + try { + filteredData = data.where((item) { + bool matchesCriteria = true; + if (event.passwordName != null && event.passwordName!.isNotEmpty) { + final bool matchesName = item.passwordName != null && + item.passwordName.contains(event.passwordName); + if (!matchesName) { + matchesCriteria = false; + } + } + if (event.startTime != null && event.endTime != null) { + final int? effectiveTime = int.tryParse(item.effectiveTime.toString()); + final int? invalidTime = int.tryParse(item.invalidTime.toString()); + if (effectiveTime == null || invalidTime == null) { + matchesCriteria = false; + } else { + final bool matchesStartTime = effectiveTime >= event.startTime!; + final bool matchesEndTime = invalidTime <= event.endTime!; + if (!matchesStartTime || !matchesEndTime) { + matchesCriteria = false; + } + } + } + if (event.selectedTabIndex == 1 && item.passwordStatus.value != 'To Be Effective') { + matchesCriteria = false; + } else if (event.selectedTabIndex == 2 && item.passwordStatus.value != 'Effective') { + matchesCriteria = false; + } else if (event.selectedTabIndex == 3 && item.passwordStatus.value != 'Expired') { + matchesCriteria = false; + } + return matchesCriteria; + }).toList(); + emit(TableLoaded(filteredData)); + } catch (e) { + emit(FailedState(e.toString())); + } + } + + resetSearch(ResetSearch event, Emitter emit) async{ + emit(AccessLoaded()); + startTime = 'Start Time'; + endTime = 'End Time'; + passwordName.clear(); + selectedIndex=0; + effectiveTimeTimeStamp=null; + expirationTimeTimeStamp=null; + add(FetchTableData()); + } + + String timestampToDate(dynamic timestamp) { + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp) * 1000); + return "${dateTime.year}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.day.toString().padLeft(2, '0')}"; + } + + Future onTabChanged(TabChangedEvent event, Emitter emit) async { + try { + emit(AccessLoaded()); + selectedIndex = event.selectedIndex; + switch (selectedIndex) { + case 0: // All + filteredData = data; + break; + case 1: // To Be Effective + filteredData = data.where((item) => item.passwordStatus.value == "To Be Effective").toList(); + break; + case 2: // Effective + filteredData = data.where((item) => item.passwordStatus.value == "Effective").toList(); + break; + case 3: // Expired + filteredData = data.where((item) => item.passwordStatus.value == "Expired").toList(); + break; + default: + filteredData = data; + } + add(FilterDataEvent( + selectedTabIndex: selectedIndex, + passwordName: passwordName.text.toLowerCase(), + startTime: effectiveTimeTimeStamp, + endTime: expirationTimeTimeStamp + )); + emit(TableLoaded(filteredData)); + } catch (e) { + emit(FailedState(e.toString())); + } + } + +} diff --git a/lib/pages/access_management/bloc/access_event.dart b/lib/pages/access_management/bloc/access_event.dart new file mode 100644 index 00000000..f2f631b4 --- /dev/null +++ b/lib/pages/access_management/bloc/access_event.dart @@ -0,0 +1,46 @@ + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +abstract class AccessEvent extends Equatable { + const AccessEvent(); + + @override + List get props => []; +} +class FetchTableData extends AccessEvent {} +class ResetSearch extends AccessEvent {} + +class TabChangedEvent extends AccessEvent { + final int selectedIndex; + + const TabChangedEvent(this.selectedIndex); +} + + +class SelectTime extends AccessEvent { + final BuildContext context; + final bool isStart; + const SelectTime({required this.context,required this.isStart}); + @override + List get props => [context,isStart]; +} + + +class FilterDataEvent extends AccessEvent { + final String? passwordName; + final int? startTime; + final int? endTime; + final int selectedTabIndex; // Add this field + + const FilterDataEvent({ + this.passwordName, + this.startTime, + this.endTime, + required this.selectedTabIndex, // Initialize this field + + }); +} + + + diff --git a/lib/pages/access_management/bloc/access_state.dart b/lib/pages/access_management/bloc/access_state.dart new file mode 100644 index 00000000..11253d1f --- /dev/null +++ b/lib/pages/access_management/bloc/access_state.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/model/password_model.dart'; + +abstract class AccessState extends Equatable { + const AccessState(); + + @override + List get props => []; +} + +class AccessInitial extends AccessState {} + +class AccessLoaded extends AccessState {} +class FailedState extends AccessState { + final String message; + + FailedState(this.message); + + @override + List get props => [message]; +} + +class TableLoaded extends AccessState { + final List data; + + const TableLoaded(this.data); + + @override + List get props => [data]; +} + +class TabState extends AccessState { + final int selectedIndex; + + const TabState({required this.selectedIndex}); +} + +class ChangeTimeState extends AccessState {} + +class TimeSelectedState extends AccessState {} diff --git a/lib/pages/access_management/model/password_model.dart b/lib/pages/access_management/model/password_model.dart new file mode 100644 index 00000000..584e9b7e --- /dev/null +++ b/lib/pages/access_management/model/password_model.dart @@ -0,0 +1,54 @@ +import 'package:syncrow_web/utils/constants/const.dart'; + +class PasswordModel { + final dynamic passwordId; + final dynamic invalidTime; + final dynamic effectiveTime; + final dynamic passwordCreated; + final dynamic createdTime; + final dynamic passwordName; // New field + final AccessStatus passwordStatus; + final AccessType passwordType; + final dynamic deviceUuid; + + PasswordModel({ + this.passwordId, + this.invalidTime, + this.effectiveTime, + this.passwordCreated, + this.createdTime, + this.passwordName, // New field + required this.passwordStatus, + required this.passwordType, + this.deviceUuid, + }); + + factory PasswordModel.fromJson(Map json) { + return PasswordModel( + passwordId: json['passwordId'], + invalidTime: json['invalidTime'], + effectiveTime: json['effectiveTime'], + passwordCreated: json['passwordCreated'], + createdTime: json['createdTime'], + passwordName: json['passwordName']??'No name', // New field + passwordStatus:AccessStatusExtension.fromString(json['passwordStatus']), + passwordType:AccessTypeExtension.fromString(json['passwordType']), + deviceUuid: json['deviceUuid'], + ); + } + + Map toJson() { + return { + 'passwordId': passwordId, + 'invalidTime': invalidTime, + 'effectiveTime': effectiveTime, + 'passwordCreated': passwordCreated, + 'createdTime': createdTime, + 'passwodName': passwordName, // New field + 'passwordStatus': passwordStatus, + 'passwordType': passwordType, + 'deviceUuid': deviceUuid, + }; + } + +} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart new file mode 100644 index 00000000..d17e774b --- /dev/null +++ b/lib/pages/access_management/view/access_management.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; +import 'package:syncrow_web/pages/common/custom_table.dart'; +import 'package:syncrow_web/pages/common/date_time_widget.dart'; +import 'package:syncrow_web/pages/common/default_button.dart'; +import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/constants/const.dart'; +import 'package:syncrow_web/utils/style.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class AccessManagementPage extends StatelessWidget { + const AccessManagementPage({super.key}); + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return WebScaffold( + enableMenuSideba: false, + appBarTitle: Row( + children: [ + Text('Access Management', + style: Theme.of(context).textTheme.headlineLarge, + ) + ], + ), + appBarBody: [ + Text('Physical Access', + style: Theme.of(context).textTheme + .headlineMedium! + .copyWith(color: Colors.white), + ), + ], + scaffoldBody: BlocProvider(create: (BuildContext context) => AccessBloc()..add(FetchTableData()), + child: BlocConsumer(listener: (context, state) { + }, builder: (context, state) { + final accessBloc = BlocProvider.of(context); + final filteredData = accessBloc.filteredData; + return state is AccessLoaded? + const Center(child: CircularProgressIndicator()): + Container( + padding: EdgeInsets.all(30), + height: size.height, + width: size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: containerDecoration, + height: size.height * 0.05, + child: Flexible( + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: BlocProvider.of(context).tabs.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final isSelected = index == BlocProvider.of(context).selectedIndex; + return InkWell( + onTap: () { + BlocProvider.of(context).add(TabChangedEvent(index)); + }, + child: Container( + decoration: BoxDecoration( + color: ColorsManager.boxColor, + border: Border.all( + color: isSelected ? Colors.blue : Colors.transparent, + width: 2.0, + ), + borderRadius: index == 0 + ? const BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10)) + : index == 3 + ? const BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10)) + : null, + ), + padding: const EdgeInsets.only(left: 10, right: 10), + child: Center( + child: Text( + BlocProvider.of(context).tabs[index], + style: TextStyle( + color: isSelected ? Colors.blue : Colors.black, + ), + ), + ), + ), + ); + }, + ), + ), + ), + const SizedBox( + height: 20, + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.ideographic, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Name', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black,fontSize: 13),), + const SizedBox(height: 5,), + Container( + height:43, + width: size.width * 0.15, + decoration: containerDecoration, + child: TextFormField( + controller: accessBloc.passwordName, + style: const TextStyle(color: Colors.black), + decoration: textBoxDecoration()! + .copyWith(hintText: 'Please enter'), + ) + ), + ], + ), + const SizedBox( + width: 15, + ), + DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString:BlocProvider.of(context).startTime , + secondString:BlocProvider.of(context).endTime , + ) , + const SizedBox( + width: 15, + ), + + SizedBox( + height:45, + + width: size.width * 0.06, + child:Container( + decoration: containerDecoration, + child: DefaultButton( + onPressed: () { + accessBloc.add(FilterDataEvent( + selectedTabIndex: BlocProvider.of(context).selectedIndex, // Pass the selected tab index + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp + )); + }, borderRadius: 9, + child: const Text('Search'))), + ), + const SizedBox( + width: 10, + ), + SizedBox( + height:45, + width: size.width * 0.06, + child: Container( + decoration: containerDecoration, + child: DefaultButton( + onPressed: () { + accessBloc.add(ResetSearch()); + }, + backgroundColor: ColorsManager.whiteColors, + borderRadius: 9, + child: Text( + 'Reset', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black), + ), + ), + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + Wrap( + children: [ + Container( + width: size.width * 0.15, + decoration: containerDecoration, + child: DefaultButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const VisitorPasswordDialog(); + }, + ).then((v){ + if(v!=null){ + accessBloc.add(FetchTableData()); + } + }); + }, + borderRadius: 8, + child: const Text('+ Create Visitor Password ')), + ), + const SizedBox( + width: 10, + ), + Container( + width: size.width * 0.12, + decoration: containerDecoration, + child: DefaultButton( + borderRadius: 8, + backgroundColor: ColorsManager.whiteColors, + child: Text( + 'Admin Password', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black), + ))) + ], + ), + const SizedBox( + height: 20, + ), + Expanded( + child: DynamicTable( + isEmpty: filteredData.isEmpty , + withCheckBox: false, + size: size, + cellDecoration: containerDecoration, + headers: const [ + 'Name', + 'Access Type', + 'Access Period', + 'Accessible Device', + 'Authorizer', + 'Authorization Date & Time', + 'Access Status' + ], + data: filteredData.map((item) { + return [ + item.passwordName.toString(), + item.passwordType.value, + ('${accessBloc.timestampToDate(item.effectiveTime)} - ${accessBloc.timestampToDate(item.invalidTime)}'), + item.deviceUuid.toString(), + '', + '', + item.passwordStatus.value + ]; + }).toList(), + ) + // : const Center(child: CircularProgressIndicator()), + ) + ], + ), + ); + }))); + } +} + + diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 00f6227f..125c2817 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -23,26 +23,34 @@ class AuthBloc extends Bloc { on(_onUpdateTimer); on(_passwordVisible); on(_fetchRegion); + on(selectRegion); + on(checkEnable); + on(changeValidate); } - ////////////////////////////// forget password ////////////////////////////////// + ////////////////////////////// forget password ////////////////////////////////// final TextEditingController forgetEmailController = TextEditingController(); - final TextEditingController forgetPasswordController = TextEditingController(); + final TextEditingController forgetPasswordController = + TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); + late bool checkValidate = false; + Timer? _timer; int _remainingTime = 0; - List? regionList; + List? regionList = [RegionModel(name: 'name', id: 'id')]; - Future _onStartTimer(StartTimerEvent event, Emitter emit) async { + Future _onStartTimer( + StartTimerEvent event, Emitter emit) async { if (_validateInputs(emit)) return; if (_timer != null && _timer!.isActive) { return; } - _remainingTime = 60; + _remainingTime = 1; add(UpdateTimerEvent( remainingTime: _remainingTime, isButtonEnabled: false)); - await AuthenticationAPI.sendOtp(email: forgetEmailController.text); + _remainingTime = (await AuthenticationAPI.sendOtp( + email: forgetEmailController.text, regionUuid: regionUuid))!; _timer = Timer.periodic(const Duration(seconds: 1), (timer) { _remainingTime--; if (_remainingTime <= 0) { @@ -50,7 +58,8 @@ class AuthBloc extends Bloc { add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true)); } else { add(UpdateTimerEvent( - remainingTime: _remainingTime, isButtonEnabled: false)); + remainingTime: _remainingTime, + isButtonEnabled: false)); } }); } @@ -64,7 +73,7 @@ class AuthBloc extends Bloc { ChangePasswordEvent event, Emitter emit) async { try { emit(LoadingForgetState()); - bool response = await AuthenticationAPI.verifyOtp( + var response = await AuthenticationAPI.verifyOtp( email: forgetEmailController.text, otpCode: forgetOtp.text); if (response == true) { await AuthenticationAPI.forgetPassword( @@ -72,12 +81,27 @@ class AuthBloc extends Bloc { email: forgetEmailController.text); _timer?.cancel(); emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); + emit(SuccessForgetState()); + } else if (response == "You entered wrong otp") { + forgetValidate = 'Wrong one time password.'; + emit(AuthInitialState()); + }else if (response == "OTP expired") { + forgetValidate = 'One time password has been expired.'; + emit(AuthInitialState()); } - emit(SuccessForgetState()); } catch (failure) { - emit(FailureForgetState(error: failure.toString())); + // forgetValidate='Invalid Credentials!'; + emit(AuthInitialState()); + // emit(FailureForgetState(error: failure.toString())); } } +//925207 + String? validateCode(String? value) { + if (value == null || value.isEmpty) { + return 'Code is required'; + } + return null; + } void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) { emit(TimerState( @@ -85,10 +109,6 @@ class AuthBloc extends Bloc { remainingTime: event.remainingTime)); } - - - - ///////////////////////////////////// login ///////////////////////////////////// final TextEditingController loginEmailController = TextEditingController(); final TextEditingController loginPasswordController = TextEditingController(); @@ -99,12 +119,15 @@ class AuthBloc extends Bloc { String maskedEmail = ''; String otpCode = ''; String validate = ''; + String forgetValidate = ''; + String regionUuid = ''; static Token token = Token.emptyConstructor(); static UserModel? user; bool showValidationMessage = false; void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); + if (isChecked) { try { if (event.username.isEmpty || event.password.isEmpty) { @@ -114,14 +137,14 @@ class AuthBloc extends Bloc { } token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( - email: event.username, - password: event.password, - ), + email: event.username, + password: event.password, + regionUuid: event.regionUuid), ); } catch (failure) { - validate='Something went wrong'; - emit(const LoginFailure(error: 'Something went wrong')); - // emit(LoginFailure(error: failure.toString())); + validate = 'Invalid Credentials!'; + emit(AuthInitialState()); + // emit(const LoginFailure(error: 'Something went wrong')); return; } if (token.accessTokenIsNotEmpty) { @@ -139,6 +162,7 @@ class AuthBloc extends Bloc { emit(const LoginFailure(error: 'Something went wrong')); } } else { + emit(const LoginFailure(error: 'Accept terms and condition')); } } @@ -146,13 +170,13 @@ class AuthBloc extends Bloc { checkBoxToggle(CheckBoxEvent event, Emitter emit,) { emit(AuthLoading()); isChecked = event.newValue!; + add(CheckEnableEvent()); emit(LoginInitial()); } checkOtpCode(ChangePasswordEvent event, Emitter emit,) async { emit(LoadingForgetState()); - await AuthenticationAPI.verifyOtp( - email: forgetEmailController.text, otpCode: forgetOtp.text); + await AuthenticationAPI.verifyOtp(email: forgetEmailController.text, otpCode: forgetOtp.text); emit(SuccessForgetState()); } @@ -162,18 +186,12 @@ class AuthBloc extends Bloc { emit(PasswordVisibleState()); } - void launchURL(String url) { - - } - - + void launchURL(String url) {} /////////////////////////////////////VALIDATORS///////////////////////////////////// String? validatePassword(String? value) { if (value == null || value.isEmpty) { - return 'Password is required'; - } else if (value.length < 8) { - return 'Password must be at least 8 characters'; + return ''; } return null; } @@ -183,16 +201,21 @@ class AuthBloc extends Bloc { return 'Email is required'; } else if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { return 'Enter a valid email address'; + } else if (regionUuid == '') { + return 'Please select your region'; + } + validate=''; + return null; + } + + String? loginValidateEmail(String? value) { + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value!)) { + return ''; } return null; } - String? validateCode(String? value) { - if (value == null || value.isEmpty) { - return 'Code is required'; - } - return null; - } + bool _validateInputs(Emitter emit) { emit(LoadingForgetState()); @@ -274,25 +297,17 @@ class AuthBloc extends Bloc { return '$maskedLocalPart@$domainPart'; } - final List regions = [ - 'North America', - 'South America', - 'Europe', - 'Asia', - 'Africa', - 'Australia', - 'Antarctica', - ]; - - - static Future getTokenAndValidate() async { + static Future getTokenAndValidate() async { try { const storage = FlutterSecureStorage(); - final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; + final firstLaunch = await SharedPreferencesHelper.readBoolFromSP( + StringsManager.firstLaunch) ?? + true; if (firstLaunch) { storage.deleteAll(); } - await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false); + await SharedPreferencesHelper.saveBoolToSP( + StringsManager.firstLaunch, false); final value = await storage.read(key: Token.loginAccessTokenKey) ?? ''; if (value.isEmpty) { return 'Token not found'; @@ -318,15 +333,58 @@ class AuthBloc extends Bloc { try { emit(AuthLoading()); regionList = await AuthenticationAPI.fetchRegion(); - emit(LoginSuccess()); + emit(AuthInitialState()); } catch (e) { - emit( LoginFailure(error: e.toString())); - + emit(LoginFailure(error: e.toString())); } } + Future selectRegion(SelectRegionEvent event, Emitter emit) async { + try { + emit(AuthLoading()); + regionUuid = event.val; + emit(AuthInitialState()); + } catch (e) { + emit(FailureForgetState(error: e.toString())); + return; + } + } + String formattedTime(int time) { + final int days = (time / 86400).floor(); // 86400 seconds in a day + final int hours = ((time % 86400) / 3600).floor(); + final int minutes = (((time % 86400) % 3600) / 60).floor(); + final int seconds = (((time % 86400) % 3600) % 60).floor(); + + final String formattedTime = [ + if (days > 0) '${days}d', // Append 'd' for days + if (days > 0 || hours > 0) + hours.toString().padLeft(2, '0'), // Show hours if there are days or hours + minutes.toString().padLeft(2, '0'), + seconds.toString().padLeft(2, '0'), + ].join(':'); + + return formattedTime; + } + + bool checkEnable( CheckEnableEvent event, Emitter emit,) { + emit(AuthLoading()); + checkValidate = isChecked==true && + loginPasswordController.text.isNotEmpty && + loginEmailController.text.isNotEmpty && + regionUuid != ''; + emit(LoginInitial()); + return checkValidate; + } + + changeValidate(ChangeValidateEvent event, Emitter emit,){ + emit(AuthLoading()); + validate=''; + emit(LoginInitial()); + } + changeForgetValidate(ChangeValidateEvent event, Emitter emit,){ + emit(AuthLoading()); + forgetValidate=''; + emit(LoginInitial()); + } } - - - diff --git a/lib/pages/auth/bloc/auth_event.dart b/lib/pages/auth/bloc/auth_event.dart index 8a410555..4f80a2db 100644 --- a/lib/pages/auth/bloc/auth_event.dart +++ b/lib/pages/auth/bloc/auth_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; abstract class AuthEvent extends Equatable { const AuthEvent(); @@ -10,11 +11,12 @@ abstract class AuthEvent extends Equatable { class LoginButtonPressed extends AuthEvent { final String username; final String password; + final String regionUuid; - const LoginButtonPressed({required this.username, required this.password}); + const LoginButtonPressed({required this.username, required this.password, required this.regionUuid, }); @override - List get props => [username, password]; + List get props => [username, password,regionUuid]; } class CheckBoxEvent extends AuthEvent { @@ -51,5 +53,13 @@ class PasswordVisibleEvent extends AuthEvent{ } class RegionInitialEvent extends AuthEvent {} +class CheckEnableEvent extends AuthEvent {} +class ChangeValidateEvent extends AuthEvent {} + +class SelectRegionEvent extends AuthEvent { + final String val; + const SelectRegionEvent({required this.val}); + @override + List get props => [val]; +} -class SelectRegionEvent extends AuthEvent {} diff --git a/lib/pages/auth/bloc/auth_state.dart b/lib/pages/auth/bloc/auth_state.dart index 4b6359eb..973ee400 100644 --- a/lib/pages/auth/bloc/auth_state.dart +++ b/lib/pages/auth/bloc/auth_state.dart @@ -12,6 +12,7 @@ class LoginInitial extends AuthState {} class AuthTokenLoading extends AuthState {} class AuthLoading extends AuthState {} +class AuthInitialState extends AuthState {} class LoginSuccess extends AuthState {} @@ -73,3 +74,12 @@ class AuthTokenError extends AuthError { class AuthSuccess extends AuthState {} class AuthTokenSuccess extends AuthSuccess {} +class TimerUpdated extends AuthState { + final String formattedTime; + final bool isButtonEnabled; + + TimerUpdated({ + required this.formattedTime, + required this.isButtonEnabled, + }); +} diff --git a/lib/pages/auth/model/login_with_email_model.dart b/lib/pages/auth/model/login_with_email_model.dart index c387b0d2..88be9808 100644 --- a/lib/pages/auth/model/login_with_email_model.dart +++ b/lib/pages/auth/model/login_with_email_model.dart @@ -1,16 +1,19 @@ class LoginWithEmailModel { final String email; final String password; + final String regionUuid; LoginWithEmailModel({ required this.email, required this.password, + required this.regionUuid, }); factory LoginWithEmailModel.fromJson(Map json) { return LoginWithEmailModel( email: json['email'], password: json['password'], + regionUuid: json['regionUuid'], ); } @@ -18,6 +21,7 @@ class LoginWithEmailModel { return { 'email': email, 'password': password, + 'regionUuid': regionUuid, }; } } diff --git a/lib/pages/auth/model/user_model.dart b/lib/pages/auth/model/user_model.dart index 674eee9a..cbae385a 100644 --- a/lib/pages/auth/model/user_model.dart +++ b/lib/pages/auth/model/user_model.dart @@ -6,19 +6,17 @@ class UserModel { static String userUuidKey = 'userUuid'; final String? uuid; final String? email; - final String? name; + final String? firstName; + final String? lastName; final String? photoUrl; - final String? phoneNumber; - final bool? isEmailVerified; - final bool? isAgreementAccepted; - UserModel({ required this.uuid, required this.email, - required this.name, + required this.firstName, + required this.lastName, required this.photoUrl, required this.phoneNumber, required this.isEmailVerified, @@ -29,7 +27,8 @@ class UserModel { return UserModel( uuid: json['id'], email: json['email'], - name: json['name'], + firstName: json['firstName'], + lastName: json['lastName'], photoUrl: json['photoUrl'], phoneNumber: json['phoneNumber'], isEmailVerified: json['isEmailVerified'], @@ -46,7 +45,8 @@ class UserModel { return UserModel( uuid: tempJson['uuid'].toString(), email: tempJson['email'], - name: null, + firstName: null, + lastName: null, photoUrl: null, phoneNumber: null, isEmailVerified: null, @@ -58,7 +58,8 @@ class UserModel { return { 'id': uuid, 'email': email, - 'name': name, + 'firstName': firstName, + 'lastName': lastName, 'photoUrl': photoUrl, 'phoneNumber': phoneNumber, 'isEmailVerified': isEmailVerified, diff --git a/lib/pages/auth/view/forget_password_page.dart b/lib/pages/auth/view/forget_password_page.dart index da09a888..f90b2ef4 100644 --- a/lib/pages/auth/view/forget_password_page.dart +++ b/lib/pages/auth/view/forget_password_page.dart @@ -12,7 +12,7 @@ class ForgetPasswordPage extends StatelessWidget { Widget build(BuildContext context) { return const ResponsiveLayout( desktopBody: ForgetPasswordWebPage(), - mobileBody:ForgetPasswordMobilePage() + mobileBody:ForgetPasswordWebPage() ); } } diff --git a/lib/pages/auth/view/forget_password_web_page.dart b/lib/pages/auth/view/forget_password_web_page.dart index d24691b6..a2a939e3 100644 --- a/lib/pages/auth/view/forget_password_web_page.dart +++ b/lib/pages/auth/view/forget_password_web_page.dart @@ -4,24 +4,30 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_event.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_state.dart'; +import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/common/default_button.dart'; import 'package:syncrow_web/pages/common/first_layer.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; + class ForgetPasswordWebPage extends StatelessWidget { const ForgetPasswordWebPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: BlocProvider( - create: (context) => AuthBloc(), + create: (context) => AuthBloc()..add(RegionInitialEvent()), child: BlocConsumer( listener: (context, state) { - if (state is SuccessForgetState){ + if (state is SuccessForgetState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Change Password Successfully '), + ), + ); Navigator.of(context).pop(); - } - else if (state is FailureForgetState) { + } else if (state is FailureForgetState) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.error), @@ -30,11 +36,7 @@ class ForgetPasswordWebPage extends StatelessWidget { } }, builder: (context, state) { - if (state is LoadingForgetState) { - return const Center(child: CircularProgressIndicator()); - } else { return _buildForm(context, state); - } }, ), ), @@ -45,263 +47,317 @@ class ForgetPasswordWebPage extends StatelessWidget { late ScrollController _scrollController; _scrollController = ScrollController(); void _scrollToCenter() { - final double middlePosition = _scrollController.position.maxScrollExtent / 2; + final double middlePosition = + _scrollController.position.maxScrollExtent / 2; _scrollController.animateTo( middlePosition, duration: const Duration(seconds: 1), curve: Curves.easeInOut, ); } + WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToCenter(); }); final forgetBloc = BlocProvider.of(context); - Size size = MediaQuery.of(context).size; + Size size = MediaQuery.of(context).size; return FirstLayer( - second: Center( - child: ListView( - shrinkWrap: true, - controller: _scrollController, - children: [ - Container( - padding: EdgeInsets.all(size.width*0.02), - margin: EdgeInsets.all(size.width*0.09), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - child: Center( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - Expanded( - flex: 3, - child: SvgPicture.asset( - Assets.loginLogo, - ), - ), - const Spacer(), - Expanded( - flex: 3, - child: Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(30)), - border: Border.all(color: ColorsManager.graysColor.withOpacity(0.2)), + second: Center( + child: Stack( + children: [ + if (state is AuthLoading) + const Center(child: CircularProgressIndicator()), + ListView( + shrinkWrap: true, + controller: _scrollController, + children: [ + Container( + padding: EdgeInsets.all(size.width * 0.02), + margin: EdgeInsets.all(size.width * 0.09), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Center( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + Expanded( + flex: 3, + child: SvgPicture.asset( + Assets.loginLogo, ), - child: Form( - key: forgetBloc.forgetFormKey, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: size.width*0.02, - vertical: size.width*0.003), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - const Text( - 'Forget Password', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), - Text( - 'Please fill in your account information to\nretrieve your password', - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Country/Region", - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 10), - SizedBox( - child: DropdownButtonFormField( - validator: forgetBloc.validateRegion, - icon: const Icon( - Icons.keyboard_arrow_down_outlined, - ), - decoration: textBoxDecoration()!.copyWith( - hintText: null, - ), - hint: SizedBox( - width: size.width * 0.11, - child: const Align( - alignment: Alignment.centerLeft, - child: Text( - 'Select your region/country', - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Expanded( + flex: 3, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: + const BorderRadius.all(Radius.circular(30)), + border: Border.all( + color: ColorsManager.graysColor.withOpacity(0.2)), + ), + child: Form( + key: forgetBloc.forgetFormKey, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: size.width * 0.02, + vertical: size.width * 0.003), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + const Text( + 'Forget Password', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Text( + 'Please fill in your account information to\nretrieve your password', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Country/Region", + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + SizedBox( + child: DropdownButtonFormField( + validator: forgetBloc.validateRegion, + icon: const Icon( + Icons.keyboard_arrow_down_outlined, + ), + decoration: textBoxDecoration()!.copyWith( + hintText: null, + ), + hint: SizedBox( + width: size.width * 0.12, + child: const Align( + alignment: Alignment.centerLeft, + child: Text( + 'Select your region/country', + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), ), ), + isDense: true, + style: + const TextStyle(color: Colors.black), + items: forgetBloc.regionList! + .map((RegionModel region) { + return DropdownMenuItem( + value: region.id, + child: SizedBox( + width: size.width*0.06, + + child: Text(region.name)), + ); + }).toList(), + onChanged: (String? value) { + forgetBloc.add(SelectRegionEvent( + val: value!, + )); + }, ), - isDense: true, - style: const TextStyle(color: Colors.black), - items: forgetBloc.regions.map((String region) { - return DropdownMenuItem( - value: region, - child: Text(region), - ); - }).toList(), - onChanged: (String? value) { - print(value); - }, + ) + ], + ), + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Account", + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14,fontWeight: FontWeight.w400), + ), - ) - ], - ), - const SizedBox(height: 20), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("Account", - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 10), - SizedBox( - child: TextFormField( - validator: forgetBloc.validateEmail, - controller: forgetBloc.forgetEmailController, - decoration: textBoxDecoration()!.copyWith(hintText: 'Enter your email'), - style: const TextStyle(color: Colors.black), + const SizedBox(height: 10), + SizedBox( + child: TextFormField( + validator: forgetBloc.validateEmail, + controller: + forgetBloc.forgetEmailController, + decoration: textBoxDecoration()!.copyWith( + hintText: 'Enter your email'), + style: + const TextStyle(color: Colors.black), + ), ), - ), - ], - ), - const SizedBox(height: 20.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("One Time Password", - style: Theme.of(context).textTheme.bodySmall,), - const SizedBox(height: 10), - SizedBox( - child: TextFormField( - validator: forgetBloc.validateCode, - keyboardType: TextInputType.visiblePassword, - controller: forgetBloc.forgetOtp, - decoration: textBoxDecoration()!.copyWith( - hintText: 'Enter Code', - suffixIcon: SizedBox( - width: 100, - child: Center( - child: InkWell( - onTap: () { - BlocProvider.of(context).add(StartTimerEvent()); - }, - child: Text( - 'Get Code ${state is TimerState && !state.isButtonEnabled ? "(${state.remainingTime.toString()})" : ""}', - style: TextStyle( - color: state is TimerState && !state.isButtonEnabled - ? Colors.grey - : ColorsManager.btnColor, + ], + ), + const SizedBox(height: 20.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "One Time Password", + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14,fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + SizedBox( + child: TextFormField( + validator: forgetBloc.validateCode, + keyboardType: + TextInputType.visiblePassword, + controller: forgetBloc.forgetOtp, + decoration: textBoxDecoration()!.copyWith( + hintText: 'Enter Code', + suffixIcon: SizedBox( + width: 100, + child: Center( + child: InkWell( + onTap:state is TimerState && !state.isButtonEnabled && state.remainingTime!=1?null: () { + forgetBloc.add(StartTimerEvent()); + }, + child: Text( + 'Get Code ${state is TimerState && !state.isButtonEnabled && state.remainingTime!=1? "(${forgetBloc.formattedTime(state.remainingTime)}) " : ""}', + style: TextStyle( + color: state is TimerState && + !state.isButtonEnabled + ? Colors.grey + : ColorsManager.btnColor, + ), ), ), ), ), ), + style: + const TextStyle(color: Colors.black), ), - style: const TextStyle(color: Colors.black), ), - ), - ], - ), - const SizedBox(height: 20.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("Password", - style: Theme.of(context).textTheme.bodySmall,), - const SizedBox(height: 10), - SizedBox( - child: TextFormField( - validator: forgetBloc.passwordValidator, - keyboardType: TextInputType.visiblePassword, - controller: forgetBloc.forgetPasswordController, - decoration: textBoxDecoration()!.copyWith( - hintText: 'At least 8 characters', - ), - style: const TextStyle(color: Colors.black), - ), - ), - ], - ), - const SizedBox( - height: 10, - ), - const SizedBox(height: 20.0), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: size.width * 0.2, - child: DefaultButton( - backgroundColor: ColorsManager.btnColor, - child: const Text('Submit'), - onPressed: () { - if (forgetBloc.forgetFormKey.currentState!.validate()) { - forgetBloc.add(ChangePasswordEvent()); - } - }, - ), - ), - ], - ), - const SizedBox(height: 10.0), - SizedBox(child: Text(forgetBloc.validate, - style: const TextStyle(fontWeight: FontWeight.w700,color: ColorsManager.red ),),), - SizedBox(height: 10,), - SizedBox( - width: size.width * 0.2, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Flexible( + if (forgetBloc.forgetValidate != '') // Check if there is a validation message + Padding( + padding: const EdgeInsets.only(top: 8.0), child: Text( - "Do you have an account? ", - style: TextStyle(color: Colors.white), - )), - InkWell( - onTap: () { - Navigator.pop(context); - }, - child: const Flexible( - child: Text( - "Sign in", - )), + forgetBloc.forgetValidate, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 10, + fontWeight: FontWeight.w700 + ), + ), + ), + ], + ), + const SizedBox(height: 20.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Password", + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14,fontWeight: FontWeight.w400), + ), + const SizedBox(height: 10), + SizedBox( + child: TextFormField( + validator: forgetBloc.passwordValidator, + keyboardType: TextInputType.visiblePassword, + controller: forgetBloc.forgetPasswordController, + decoration: textBoxDecoration()!.copyWith( + hintText: 'At least 8 characters', + ), + style: const TextStyle(color: Colors.black), + ), ), ], ), - ) - ], + const SizedBox( + height: 10, + ), + const SizedBox(height: 20.0), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: size.width * 0.2, + child: DefaultButton( + backgroundColor: ColorsManager.btnColor, + child: const Text('Submit'), + onPressed: () { + if (forgetBloc.forgetFormKey.currentState!.validate()) { + forgetBloc.add(ChangePasswordEvent()); + } + }, + ), + ), + ], + ), + const SizedBox(height: 10.0), + SizedBox( + child: Text( + forgetBloc.validate, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: ColorsManager.red), + ), + ), + SizedBox( + height: 10, + ), + SizedBox( + width: size.width * 0.2, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Flexible( + child: Text( + "Do you have an account? ", + style: TextStyle(color: Colors.white), + )), + InkWell( + onTap: () { + forgetBloc.add(StopTimerEvent()); + Navigator.pop(context); + }, + child: const Flexible( + child: Text( + "Sign in", + )), + ), + ], + ), + ), + const SizedBox(height: 15.0), + ], + ), ), ), ), ), - ), - const Spacer(), - ], + const Spacer(), + ], + ), ), ), - ), - ], - ), - ) - ); + ], + ), + ], + ), + )); } } diff --git a/lib/pages/auth/view/login_mobile_page.dart b/lib/pages/auth/view/login_mobile_page.dart index a2877bf5..d6544a98 100644 --- a/lib/pages/auth/view/login_mobile_page.dart +++ b/lib/pages/auth/view/login_mobile_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_event.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_state.dart'; +import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/view/forget_password_page.dart'; import 'package:syncrow_web/pages/common/default_button.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -147,15 +148,13 @@ class LoginMobilePage extends StatelessWidget { ), isDense: true, style: const TextStyle(color: Colors.black), - items: - loginBloc.regions.map((String region) { + items:loginBloc.regionList!.map((RegionModel region) { return DropdownMenuItem( - value: region, - child: Text(region), + value: region.name, + child: Text(region.name), ); }).toList(), onChanged: (String? value) { - print(value); }, ), ) @@ -296,14 +295,12 @@ class LoginMobilePage extends StatelessWidget { : ColorsManager.grayColor, child: const Text('Sign in'), onPressed: () { - if (loginBloc.loginFormKey.currentState! - .validate()) { + if (loginBloc.loginFormKey.currentState!.validate()) { loginBloc.add( LoginButtonPressed( - username: - loginBloc.loginEmailController.text, - password: - loginBloc.loginPasswordController.text, + regionUuid:'' , + username: loginBloc.loginEmailController.text, + password: loginBloc.loginPasswordController.text, ), ); } diff --git a/lib/pages/auth/view/login_page.dart b/lib/pages/auth/view/login_page.dart index 58b07299..92487fe4 100644 --- a/lib/pages/auth/view/login_page.dart +++ b/lib/pages/auth/view/login_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:syncrow_web/pages/auth/view/login_mobile_page.dart'; import 'package:syncrow_web/pages/auth/view/login_web_page.dart'; import 'package:syncrow_web/utils/responsive_layout.dart'; @@ -11,7 +10,7 @@ class LoginPage extends StatelessWidget { Widget build(BuildContext context) { return const ResponsiveLayout( desktopBody: LoginWebPage(), - mobileBody:LoginMobilePage() + mobileBody:LoginWebPage() ); } } diff --git a/lib/pages/auth/view/login_web_page.dart b/lib/pages/auth/view/login_web_page.dart index 1a473df7..d8b22d38 100644 --- a/lib/pages/auth/view/login_web_page.dart +++ b/lib/pages/auth/view/login_web_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_event.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_state.dart'; +import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/view/forget_password_page.dart'; import 'package:syncrow_web/pages/common/default_button.dart'; import 'package:syncrow_web/pages/common/first_layer.dart'; @@ -28,7 +29,7 @@ class _LoginWebPageState extends State { Widget build(BuildContext context) { return Scaffold( body: BlocProvider( - create: (BuildContext context) => AuthBloc(), + create: (BuildContext context) => AuthBloc()..add(RegionInitialEvent()), child: BlocConsumer( listener: (context, state) { if (state is LoginSuccess) { @@ -47,11 +48,7 @@ class _LoginWebPageState extends State { } }, builder: (context, state) { - if (state is AuthLoading) { - return const Center(child: CircularProgressIndicator()); - } else { - return _buildLoginForm(context,state); - } + return _buildLoginForm(context,state); }, ), ), @@ -74,268 +71,324 @@ class _LoginWebPageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToCenter(); }); - return FirstLayer( - second: Center( - child: ListView( - controller: _scrollController, - shrinkWrap: true, - children: [ - Container( - padding: EdgeInsets.all(size.width*0.02) , - margin: EdgeInsets.all(size.width*0.09), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - child: Center( - child:Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - Expanded( - flex: 3, - child: SvgPicture.asset( - Assets.loginLogo, - ), - ), - const Spacer(), - Expanded( - flex: 3, - child: Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(30)), - border: Border.all(color: ColorsManager.graysColor.withOpacity(0.2))), - child: Form( - key: loginBloc.loginFormKey, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: size.width*0.02, - vertical: size.width*0.003), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 40), - Text( - 'Login', - style:Theme.of(context).textTheme.headlineLarge), - SizedBox(height: size.height*0.03), - Column( + return Stack( + children: [ + FirstLayer( + second: Center( + child: ListView( + controller: _scrollController, + shrinkWrap: true, + children: [ + Container( + padding: EdgeInsets.all(size.width*0.02) , + margin: EdgeInsets.all(size.width*0.09), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Center( + child:Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + Expanded( + flex: 3, + child: SvgPicture.asset( + Assets.loginLogo, + ), + ), + const Spacer(), + Expanded( + flex: 3, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(30)), + border: Border.all(color: ColorsManager.graysColor.withOpacity(0.2))), + child: Form( + key: loginBloc.loginFormKey, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: size.width*0.02, + vertical: size.width*0.003), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Country/Region", - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 10,), - SizedBox( - child: DropdownButtonFormField( - validator:loginBloc.validateRegion , - icon: const Icon( - Icons.keyboard_arrow_down_outlined, + children: [ + const SizedBox(height: 40), + Text( + 'Login', + style:Theme.of(context).textTheme.headlineLarge), + SizedBox(height: size.height*0.03), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Country/Region", + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14,fontWeight: FontWeight.w400), ), - decoration: textBoxDecoration()!.copyWith( - hintText: null,), - hint: SizedBox( - width: size.width * 0.11, - child: const Align( - alignment: Alignment.centerLeft, + const SizedBox(height: 10,), + + SizedBox( + child: DropdownButtonFormField( + padding: EdgeInsets.zero, + value: loginBloc.regionList!.any((region) => region.id == loginBloc.regionUuid) + ? loginBloc.regionUuid + : null, + + validator: loginBloc.validateRegion, + icon: const Icon( + Icons.keyboard_arrow_down_outlined, + ), + decoration: textBoxDecoration()!.copyWith( + errorStyle: const TextStyle(height: 0), + hintText: null, + ), + hint: SizedBox( + width: size.width * 0.12, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'Select your region/country', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400), + overflow: TextOverflow.ellipsis, + ), + ), + ), + isDense: true, + style: const TextStyle(color: Colors.black), + items: loginBloc.regionList!.map((RegionModel region) { + return DropdownMenuItem( + value: region.id, + child: SizedBox( + width: size.width*0.08, + child: Text(region.name)), + ); + }).toList(), + onChanged: (String? value) { + loginBloc.add(CheckEnableEvent()); + loginBloc.add(SelectRegionEvent(val: value!)); + }, + ), + ) + + + ], + ), + const SizedBox(height: 20.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text("Email", + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14,fontWeight: FontWeight.w400), + ), + const SizedBox( + height: 10, + ), + SizedBox( + child: TextFormField( + onChanged: (value) { + loginBloc.add(CheckEnableEvent()); + // print(loginBloc.checkEnable()); + }, + validator:loginBloc.loginValidateEmail , + controller:loginBloc.loginEmailController, + decoration: textBoxDecoration()!.copyWith( + errorStyle: const TextStyle(height: 0), // Hide the error text space + hintText: 'Enter your email address', + hintStyle: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400) + ), + style: const TextStyle(color: Colors.black), + ), + ), + ], + ), + const SizedBox(height: 20.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text("Password", + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 14,fontWeight: FontWeight.w400), + ), + const SizedBox( + height: 10, + ), + SizedBox( + child: TextFormField( + onChanged: (value) { + loginBloc.add(CheckEnableEvent()); + }, + validator:loginBloc.validatePassword, + obscureText:loginBloc.obscureText, + keyboardType: TextInputType.visiblePassword, + controller:loginBloc.loginPasswordController, + decoration: textBoxDecoration()!.copyWith( + hintText: 'At least 8 characters', + hintStyle: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400), + suffixIcon: IconButton(onPressed: () { + loginBloc.add(PasswordVisibleEvent(newValue: loginBloc.obscureText)); + }, + icon: SizedBox( + child: SvgPicture.asset( + loginBloc.obscureText? + Assets.visiblePassword : + Assets.invisiblePassword, + height: 15, + width: 15, + ), + ), + ), + errorStyle: const TextStyle(height: 0), // Hide the error text space + ), + style: const TextStyle(color: Colors.black), + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + SizedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ForgetPasswordPage(),)); + }, child: Text( - 'Select your region/country', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 14), - overflow: TextOverflow.ellipsis, + "Forgot Password?", + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black,fontSize: 14,fontWeight: FontWeight.w400), + ), + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Transform.scale( + scale: 1.2, // Adjust the scale as needed + child: Checkbox( + fillColor: MaterialStateProperty.all(Colors.white), + activeColor: Colors.white, + value:loginBloc.isChecked, + checkColor: Colors.black, + shape: const CircleBorder(), + onChanged: (bool? newValue) { + loginBloc.add(CheckBoxEvent(newValue: newValue)); + }, + ), + ), + SizedBox( + width:size.width * 0.14, + child: RichText( + text: TextSpan( + text: 'Agree to ', + style: const TextStyle(color: Colors.white), + children: [ + TextSpan( + text: '(Terms of Service)', + style: const TextStyle( + color: Colors.black,), + recognizer: TapGestureRecognizer() + ..onTap = () { + loginBloc.launchURL( + 'https://example.com/terms'); + }, + ), + TextSpan( + text: ' (Legal Statement)', + style: const TextStyle(color: Colors.black), + recognizer: TapGestureRecognizer() + ..onTap = () { + loginBloc.launchURL( + 'https://example.com/legal'); + }, + ), + TextSpan( + text: ' (Privacy Statement)', + style: const TextStyle( + color: Colors.black), + recognizer: TapGestureRecognizer() + ..onTap = () { + loginBloc.launchURL( + 'https://example.com/privacy'); + }, + ), + ], ), ), ), - isDense: true, - style: const TextStyle(color: Colors.black), - items:loginBloc.regions.map((String region) { - return DropdownMenuItem( - value: region, - child: Text(region), - ); - }).toList(), - onChanged: (String? value) {}, - ), - ) - ], - ), - const SizedBox(height: 20.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("Email", - style: Theme.of(context).textTheme.bodySmall, + ], ), - const SizedBox( - height: 10, - ), - SizedBox( - child: TextFormField( - validator:loginBloc.validateEmail , - controller:loginBloc.loginEmailController, - decoration: textBoxDecoration()!.copyWith(hintText: 'Enter your email'), - style: const TextStyle(color: Colors.black), - ), - ), - ], - ), - const SizedBox(height: 20.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("Password", style: Theme.of(context).textTheme.bodySmall,), - const SizedBox( - height: 10, - ), - SizedBox( - child: TextFormField( - validator:loginBloc.validatePassword, - obscureText:loginBloc.obscureText, - keyboardType: TextInputType.visiblePassword, - controller:loginBloc.loginPasswordController, - decoration: textBoxDecoration()!.copyWith( - hintText: 'At least 8 characters', - suffixIcon: IconButton(onPressed: () { - loginBloc.add(PasswordVisibleEvent(newValue: loginBloc.obscureText)); - }, - icon: SizedBox( - child: SvgPicture.asset( - loginBloc.obscureText? - Assets.visiblePassword : - Assets.invisiblePassword, - height: 15, - width: 15, - ), - ), - ) - ), - style: const TextStyle(color: Colors.black), - ), - ), - ], - ), - const SizedBox( - height: 20, - ), - SizedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - InkWell( - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ForgetPasswordPage(),)); - }, - child: Text( - "Forgot Password?", - style: Theme.of(context).textTheme.bodySmall, - ), - ), - ], - ), - ), - const SizedBox( - height: 20, - ), - Row( - children: [ - Transform.scale( - scale: 1.2, // Adjust the scale as needed - child: Checkbox( - fillColor: MaterialStateProperty.all(Colors.white), - activeColor: Colors.white, - value:loginBloc.isChecked, - checkColor: Colors.black, - shape: const CircleBorder(), - onChanged: (bool? newValue) { - loginBloc.add(CheckBoxEvent(newValue: newValue)); - }, - ), - ), - SizedBox( - width:size.width * 0.14, - child: RichText( - text: TextSpan( - text: 'Agree to ', - style: const TextStyle(color: Colors.white), - children: [ - TextSpan( - text: '(Terms of Service)', - style: const TextStyle( - color: Colors.black,), - recognizer: TapGestureRecognizer() - ..onTap = () { - loginBloc.launchURL( - 'https://example.com/terms'); - }, + const SizedBox(height: 20.0), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width:size.width * 0.2, + child: DefaultButton( + enabled: loginBloc.checkValidate, + child:Text('Sign in', + style: Theme.of(context).textTheme.labelLarge !.copyWith( + fontSize: 14, + color: + loginBloc.checkValidate ? + ColorsManager.whiteColors:ColorsManager.whiteColors.withOpacity(0.2), + ) ), - TextSpan( - text: ' (Legal Statement)', - style: const TextStyle(color: Colors.black), - recognizer: TapGestureRecognizer() - ..onTap = () { - loginBloc.launchURL( - 'https://example.com/legal'); - }, - ), - TextSpan( - text: ' (Privacy Statement)', - style: const TextStyle( - color: Colors.black), - recognizer: TapGestureRecognizer() - ..onTap = () { - loginBloc.launchURL( - 'https://example.com/privacy'); - }, - ), - ], + onPressed: () { + if(loginBloc.loginFormKey.currentState!.validate() ){ + loginBloc.add(LoginButtonPressed( + regionUuid:loginBloc.regionUuid, + username: loginBloc.loginEmailController.text, + password: loginBloc.loginPasswordController.text, + )); + }else{ + loginBloc.add(ChangeValidateEvent()); + } + }, + ), ), - ), + ], ), + const SizedBox(height: 15.0), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ SizedBox(child: Text(loginBloc.validate, + style: const TextStyle(fontWeight: FontWeight.w700,color: ColorsManager.red ),),)],) ], ), - const SizedBox(height: 20.0), - SizedBox( - width:size.width * 0.2, - child: DefaultButton( - backgroundColor: loginBloc.isChecked? - ColorsManager.btnColor:ColorsManager.grayColor, - child: const Text('Sign in'), - onPressed: () { - if (loginBloc.loginFormKey.currentState!.validate()) { - loginBloc.add(LoginButtonPressed( - username: loginBloc.loginEmailController.text, - password: loginBloc.loginPasswordController.text, - ), - ); - } - }, - ), - ), - const SizedBox(height: 15.0), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ SizedBox(child: Text(loginBloc.validate, - style: const TextStyle(fontWeight: FontWeight.w700,color: ColorsManager.red ),),)],) - ], - ), - ), - ) - )), - const Spacer(), - ], - ),), + ), + ) + )), + const Spacer(), + ], + ),), + ), + ], ), - ], + ), ), - ), + if (state is AuthLoading) + const Center(child: CircularProgressIndicator()) + ], ); } } diff --git a/lib/pages/common/custom_dialog.dart b/lib/pages/common/custom_dialog.dart new file mode 100644 index 00000000..3b667811 --- /dev/null +++ b/lib/pages/common/custom_dialog.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +Future showCustomDialog({ + required BuildContext context, + required String message, + String? title, + String? iconPath, + double? dialogHeight, + double? iconHeight, + double? iconWidth, + VoidCallback? onOkPressed, + bool barrierDismissible = false, required actions, +}) { + return showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + final size = MediaQuery.of(context).size; + return AlertDialog( + alignment: Alignment.center, + content: SizedBox( + height: dialogHeight ?? size.height * 0.15, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (iconPath != null) + SvgPicture.asset( + iconPath, + height: iconHeight ?? 35, + width: iconWidth ?? 35, + ), + if (title != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + title, + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w400, + color: Colors.black), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + message, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.black), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + actionsAlignment: MainAxisAlignment.center, + actions: [ + TextButton( + onPressed: onOkPressed ?? () => Navigator.of(context).pop(), + child: Text( + 'OK', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + fontSize: 16), + ), + ), + ], + ); + }, + ); +} diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart new file mode 100644 index 00000000..76e3b663 --- /dev/null +++ b/lib/pages/common/custom_table.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DynamicTable extends StatefulWidget { + final List headers; + final List> data; + final BoxDecoration? headerDecoration; + final BoxDecoration? cellDecoration; + final Size size; + final bool withCheckBox; + final bool isEmpty; + final void Function(bool?)? selectAll; + final void Function(int, bool?)? onRowCheckboxChanged; + final List? initialSelectedIds; + + const DynamicTable({ + super.key, + required this.headers, + required this.data, + required this.size, + required this.isEmpty, + required this.withCheckBox, + this.headerDecoration, + this.cellDecoration, + this.selectAll, + this.onRowCheckboxChanged, + this.initialSelectedIds, + }); + + @override + _DynamicTableState createState() => _DynamicTableState(); +} + +class _DynamicTableState extends State { + late List _selected; + bool _selectAll = false; + + @override + void initState() { + super.initState(); + _selected = List.generate(widget.data.length, (index) { + return widget.initialSelectedIds != null && + widget.initialSelectedIds!.contains(widget.data[index][1]); + }); + _selectAll = _selected.every((element) => element == true); + } + + void _toggleSelectAll(bool? value) { + setState(() { + _selectAll = value ?? false; + _selected = List.filled(widget.data.length, _selectAll); + if (widget.selectAll != null) { + widget.selectAll!(_selectAll); + } + }); + } + + void _toggleRowSelection(int index, bool? value) { + setState(() { + _selected[index] = value ?? false; + _selectAll = _selected.every((element) => element == true); + if (widget.onRowCheckboxChanged != null) { + widget.onRowCheckboxChanged!(index, _selected[index]); + } + }); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: widget.cellDecoration, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: + ListView( + scrollDirection: Axis.horizontal, + children: [ + SizedBox( + width: widget.size.width, + child: Column( + children: [ + Container( + decoration: widget.headerDecoration ?? + BoxDecoration(color: Colors.grey[200]), + child: Row( + children: [ + if (widget.withCheckBox) + _buildSelectAllCheckbox(), + ...widget.headers + .map((header) => _buildTableHeaderCell(header)) + .toList(), + ], + ), + ), + widget.isEmpty? + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset( + Assets.emptyTable + ), + const SizedBox(height: 15,), + Text('No Passwords',style: Theme.of(context).textTheme.bodySmall!.copyWith(color:ColorsManager.grayColor ),) + ], + ), + ], + ), + ], + ), + ): + Expanded( + child: Container( + color: Colors.white, + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.data.length, + itemBuilder: (context, index) { + final row = widget.data[index]; + return Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox(index,widget.size.height*0.10), + ...row.map((cell) => + _buildTableCell(cell.toString(),widget.size.height*0.10)).toList(), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSelectAllCheckbox() { + return Container( + padding: const EdgeInsets.all(8.0), + + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: ColorsManager.boxDivider))), + child: Checkbox( + value: _selectAll, + onChanged: _toggleSelectAll, + ), + ); + } + + Widget _buildRowCheckbox(int index,size) { + return Container( + padding: const EdgeInsets.all(8.0), + + height:size , + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, + ), + )), + alignment: Alignment.centerLeft, + child: Center(child: Checkbox( + value: _selected[index], + onChanged: (bool? value) { + _toggleRowSelection(index, value); + }, + ),) + ); + + } + + Widget _buildTableHeaderCell(String title) { + return Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: ColorsManager.boxDivider))), + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(title, style: const TextStyle(fontWeight: FontWeight.w400,fontSize: 13,color: Color(0xFF999999))), + ), + ), + ); + } + + Widget _buildTableCell(String content,size) { + return Expanded( + child: Container( + height:size , + padding: const EdgeInsets.all(5.0), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, + ), + )), + alignment: Alignment.centerLeft, + child: Text( + content, + style: const TextStyle(color: Colors.black, fontSize: 10,fontWeight: FontWeight.w400), + ), + ), + ); + } +} diff --git a/lib/pages/common/custom_web_textfield.dart b/lib/pages/common/custom_web_textfield.dart new file mode 100644 index 00000000..363a1994 --- /dev/null +++ b/lib/pages/common/custom_web_textfield.dart @@ -0,0 +1,83 @@ + +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/style.dart'; +class CustomWebTextField extends StatelessWidget { + const CustomWebTextField({ + super.key, + required this.isRequired, + required this.textFieldName, + required this.controller, + this.description, + this.validator, + }); + + final bool isRequired; + final String textFieldName; + final String? description; + final TextEditingController? controller; + final String? Function(String?)? validator; + + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if(isRequired) + Row( + children: [ + Text('* ', + style: Theme.of(context) + .textTheme.bodyMedium! + .copyWith(color: Colors.red), + ), + Text(textFieldName, style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black,fontSize: 13),), + ], + ), + const SizedBox(width: 10,), + Expanded( + child: Text( + description??'', + style: Theme.of(context) + .textTheme.bodySmall! + .copyWith(fontSize: 9, + fontWeight: FontWeight.w400, + color: ColorsManager.textGray), + ), + ), + ], + ), + const SizedBox(height: 7,), + Container( + decoration: containerDecoration.copyWith( + color: const Color(0xFFF5F6F7), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius:2, + blurRadius: 3, + offset: const Offset(1, 1), // changes position of shadow + ), + ] + ), + child: TextFormField( + validator: validator, + controller: controller, + style: const TextStyle(color: Colors.black), + decoration: textBoxDecoration()! + .copyWith( + errorStyle: const TextStyle(height: 0), // Hide the error text space + + hintText: 'Please enter'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/common/date_time_widget.dart b/lib/pages/common/date_time_widget.dart new file mode 100644 index 00000000..d8fbfe51 --- /dev/null +++ b/lib/pages/common/date_time_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class DateTimeWebWidget extends StatelessWidget { + const DateTimeWebWidget({ + super.key, + required this.size, + required this.isRequired, + required this.title, + required this.startTime, + required this.endTime, + required this.firstString, + required this.secondString, + required this.icon, + }); + + final Size size; + final String title; + final bool isRequired; + final String firstString; + final String secondString; + final String icon; + final Function()? startTime; + final Function()? endTime; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + if(isRequired) + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + Text(title??'' , + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black,fontSize: 13),), + ], + ), + const SizedBox(height: 8,), + Container( + height:size.height * 0.055 , + padding: EdgeInsets.only(top: 10,bottom: 10,right: 30,left: 10), + decoration: containerDecoration, + child: FittedBox( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: startTime, + child: FittedBox( + child: Text(firstString, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor,fontSize: 12,fontWeight: FontWeight.w400),), + ) + ), + SizedBox(width: 30,), + const Icon(Icons.arrow_right_alt), + SizedBox(width: 30,), + + InkWell( + onTap:endTime, + child: FittedBox( + child: Text(secondString, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor,fontSize: 12,fontWeight: FontWeight.w400), + ), + )), + SizedBox(width: 30,), + + SvgPicture.asset( + icon, + ), + ], + ), + ], + )), + ), + ], + ); + } +} diff --git a/lib/pages/common/default_button.dart b/lib/pages/common/default_button.dart index d588acb9..181e093a 100644 --- a/lib/pages/common/default_button.dart +++ b/lib/pages/common/default_button.dart @@ -18,7 +18,6 @@ class DefaultButton extends StatelessWidget { this.height, this.padding, }); - final void Function()? onPressed; final Widget child; final double? height; @@ -28,15 +27,10 @@ class DefaultButton extends StatelessWidget { final double? padding; final bool isDone; final bool isLoading; - final TextStyle? customTextStyle; - final ButtonStyle? customButtonStyle; - final Color? backgroundColor; - final Color? foregroundColor; - @override Widget build(BuildContext context) { return ElevatedButton( @@ -65,7 +59,7 @@ class DefaultButton extends StatelessWidget { (Set states) { return enabled ? backgroundColor ?? ColorsManager.primaryColor - : Colors.grey; + : Colors.black.withOpacity(0.2); }), shape: MaterialStateProperty.all( RoundedRectangleBorder( diff --git a/lib/pages/common/hour_picker_dialog.dart b/lib/pages/common/hour_picker_dialog.dart new file mode 100644 index 00000000..718f1ebf --- /dev/null +++ b/lib/pages/common/hour_picker_dialog.dart @@ -0,0 +1,92 @@ + + +import 'package:flutter/material.dart'; + +class HourPickerDialog extends StatefulWidget { + final TimeOfDay initialTime; + const HourPickerDialog({super.key, required this.initialTime}); + + @override + _HourPickerDialogState createState() => _HourPickerDialogState(); +} + +class _HourPickerDialogState extends State { + late int _selectedHour; + bool _isPm = false; + + @override + void initState() { + super.initState(); + _selectedHour = widget.initialTime.hour > 12 ? widget.initialTime.hour - 12 : widget.initialTime.hour; + _isPm = widget.initialTime.period == DayPeriod.pm; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Select Hour'), + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + value: _selectedHour, + items: List.generate(12, (index) { + int displayHour = index + 1; + return DropdownMenuItem( + value: displayHour, + child: Text(displayHour.toString()), + ); + }), + onChanged: (value) { + setState(() { + _selectedHour = value!; + }); + }, + ), + SizedBox(width: 16.0), + DropdownButton( + value: _isPm, + items: const [ + DropdownMenuItem( + value: false, + child: Text('AM'), + ), + DropdownMenuItem( + value: true, + child: Text('PM'), + ), + ], + onChanged: (value) { + setState(() { + _isPm = value!; + }); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + int hour = _isPm ? _selectedHour + 12 : _selectedHour; + Navigator.of(context).pop(TimeOfDay(hour: hour, minute: 0)); + }, + child: const Text('OK'), + ), + ], + ); + } +} + +Future showHourPicker({ + required BuildContext context, + required TimeOfDay initialTime, +}) { + return showDialog( + context: context, + builder: (context) => HourPickerDialog(initialTime: initialTime), + ); +} diff --git a/lib/pages/common/info_dialog.dart b/lib/pages/common/info_dialog.dart new file mode 100644 index 00000000..cfd2cbd4 --- /dev/null +++ b/lib/pages/common/info_dialog.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class InfoDialog extends StatelessWidget { + final String title; + final String content; + final Size? size; + final List? actions; + + InfoDialog({ + required this.title, + required this.content, + this.actions, + this.size, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + alignment: Alignment.center, + content: SizedBox( + height: size!.height * 0.25, + child: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + child: SvgPicture.asset( + Assets.deviceNoteIcon, + height: 35, + width: 35, + ), + ), + + Text( + title, + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontSize: 30, + fontWeight: FontWeight.w400, + color: Colors.black), + ), + ], + ), + const SizedBox( + width: 15, + ), + Text( + content, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + fontSize: 18), + ), + ], + ), + ), + actionsAlignment: MainAxisAlignment.center, + actions: actions ?? + [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + } +} diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index a1475532..ebcc6b4e 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -1,23 +1,34 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:graphview/GraphView.dart'; +import 'package:syncrow_web/pages/access_management/view/access_management.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; import 'package:syncrow_web/pages/home/bloc/home_state.dart'; +import 'package:syncrow_web/pages/home/home_model/home_item_model.dart'; +import 'package:syncrow_web/services/home_api.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class HomeBloc extends Bloc { final Graph graph = Graph()..isTree = true; final BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); List sourcesList = []; List destinationsList = []; + static UserModel? user; HomeBloc() : super((HomeInitial())) { on(_createNode); + fetchUserInfo(); + } void _createNode(CreateNewNode event, Emitter emit) async { emit(HomeInitial()); sourcesList.add(event.sourceNode); destinationsList.add(event.destinationNode); - for (int i = 0; i < sourcesList.length; i++) { graph.addEdge(sourcesList[i], destinationsList[i]); } @@ -27,7 +38,89 @@ class HomeBloc extends Bloc { ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); - emit(HomeUpdateTree(graph: graph, builder: builder)); } + + + + Future fetchUserInfo() async { + try { + var uuid = + await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + user = await HomeApi().fetchUserInfo(uuid); + emit(HomeUserInfoLoaded(user!)); // Emit state after fetching user info + } catch (e) { + return; + } + } + + List homeItems = [ + HomeItemModel( + title: 'Access', + icon: Assets.accessIcon, + active: true, + onPress: (context) { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => AccessManagementPage()), + ); + }, + color: null, + ), + HomeItemModel( + title: 'Space Management', + icon: Assets.spaseManagementIcon, + active: true, + onPress: (context) { + }, + color: ColorsManager.primaryColor, + ), + HomeItemModel( + title: 'Devices', + icon: Assets.devicesIcon, + active: true, + onPress: (context) { + }, + color: ColorsManager.primaryColor, + ), + HomeItemModel( + title: 'Move in', + icon: Assets.moveinIcon, + active: false, + onPress: (context) { + }, + color: ColorsManager.primaryColor, + ), + HomeItemModel( + title: 'Construction', + icon: Assets.constructionIcon, + active: false, + onPress: (context) { + }, + color: ColorsManager.primaryColor, + ), + HomeItemModel( + title: 'Energy', + icon: Assets.energyIcon, + active: false, + onPress: (context) { + }, + color: ColorsManager.slidingBlueColor.withOpacity(0.2), + ), + HomeItemModel( + title: 'Integrations', + icon: Assets.integrationsIcon, + active: false, + onPress: (context) { + }, + color: ColorsManager.slidingBlueColor.withOpacity(0.2), + ), + HomeItemModel( + title: 'Asset', + icon: Assets.assetIcon, + active: false, + onPress: (context) { + }, + color: ColorsManager.slidingBlueColor.withOpacity(0.2), + ), + ]; } diff --git a/lib/pages/home/bloc/home_state.dart b/lib/pages/home/bloc/home_state.dart index 10c50486..7a1be0be 100644 --- a/lib/pages/home/bloc/home_state.dart +++ b/lib/pages/home/bloc/home_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:graphview/GraphView.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; abstract class HomeState extends Equatable { const HomeState(); @@ -24,3 +25,8 @@ class HomeUpdateTree extends HomeState { @override List get props => [graph, builder]; } +class HomeUserInfoLoaded extends HomeState { + final UserModel user; + + HomeUserInfoLoaded(this.user); +} diff --git a/lib/pages/home/home_model/home_item_model.dart b/lib/pages/home/home_model/home_item_model.dart new file mode 100644 index 00000000..5626cd3f --- /dev/null +++ b/lib/pages/home/home_model/home_item_model.dart @@ -0,0 +1,22 @@ + + + + +import 'package:flutter/cupertino.dart'; + +class HomeItemModel { + final String? title; + final String? icon; + final Color? color; + final bool? active; + final void Function(BuildContext context) onPress; + + + HomeItemModel({ + this.title, + this.icon, + this.color, + this.active, + required this.onPress, + }); +} \ No newline at end of file diff --git a/lib/pages/home/view/home_card.dart b/lib/pages/home/view/home_card.dart index 287bb4f4..ce152c1c 100644 --- a/lib/pages/home/view/home_card.dart +++ b/lib/pages/home/view/home_card.dart @@ -37,14 +37,16 @@ class HomeCard extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - name, - style: const TextStyle( - fontSize: 20, - color: Colors.white, - fontWeight: FontWeight.bold, + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + name, + style: const TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), ), diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index f7c4e988..39a79a57 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/pages/home/view/home_card.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -13,107 +13,67 @@ class HomeWebPage extends StatelessWidget { Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; return WebScaffold( - enableMenuSideba:false , - appBarTitle: Row( - children: [ - SvgPicture.asset( - Assets.loginLogo, - width: 150, - ), - ], - ), - scaffoldBody: BlocProvider( - create: (context) => HomeBloc(), - child: SizedBox( - height: size.height, - width: size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: size.height * 0.1), - const Text( - 'ACCESS YOUR APPS', - style: TextStyle(fontSize: 40, fontWeight: FontWeight.w700), - ), - const SizedBox(height: 30), - Expanded( - flex: 4, - child: SizedBox( - height: size.height * 0.6, - width: size.width * 0.68, - child: GridView.builder( - itemCount: 8, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - crossAxisSpacing: 20.0, - mainAxisSpacing: 20.0, - childAspectRatio: 1.5, - ), - itemBuilder: (context, index) { - return HomeCard( - index:index, - active:ceilingSensorButtons[index]['active'], - name: ceilingSensorButtons[index]['title'], - img:ceilingSensorButtons[index]['icon'] , - onTap: () { - - },); - }, - ), - ), - ), - ], - ), + enableMenuSideba: false, + appBarTitle: Row( + children: [ + SvgPicture.asset( + Assets.loginLogo, + width: 150, + ), + ], ), - ), - ); + scaffoldBody: BlocProvider( + create: (context) => HomeBloc(), + child: BlocConsumer( + listener: (BuildContext context, state) {}, + builder: (context, state) { + final homeBloc = BlocProvider.of(context); + return SizedBox( + height: size.height, + width: size.width, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: size.height * 0.1), + Text( + 'ACCESS YOUR APPS', + style: Theme.of(context) + .textTheme.headlineLarge! + .copyWith(color: Colors.black, fontSize: 40), + ), + const SizedBox(height: 30), + Expanded( + flex: 4, + child: SizedBox( + height: size.height * 0.6, + width: size.width * 0.68, + child: GridView.builder( + itemCount: 8, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 20.0, + mainAxisSpacing: 20.0, + childAspectRatio: 1.5, + ), + itemBuilder: (context, index) { + return HomeCard( + index: index, + active: homeBloc.homeItems[index].active!, + name: homeBloc.homeItems[index].title!, + img: homeBloc.homeItems[index].icon!, + onTap: () => homeBloc.homeItems[index].onPress(context), + ); + }, + ), + ), + ), + ], + ), + ); + }, + ), + )); } - - dynamic ceilingSensorButtons = - [ - { - 'title': 'Access', - 'icon': Assets.accessIcon, - 'active': true, - }, - { - 'title': 'Space\nManagement', - 'icon': Assets.spaseManagementIcon, - 'color': ColorsManager.primaryColor, - 'active': true, - }, - { - 'title': 'Devices', - 'icon':Assets.devicesIcon, - 'active': true, - }, - { - 'title': 'Move in', - 'icon': Assets.moveinIcon, - 'active': false, - }, - { - 'title': 'Construction', - 'icon': Assets.constructionIcon, - 'active': false, - }, - { - 'title': 'Energy', - 'icon': Assets.energyIcon, - 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - 'active': false, - }, - { - 'title': 'Integrations', - 'icon': Assets.integrationsIcon, - 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - 'active': false, - }, { - 'title': 'Asset', - 'icon': Assets.assetIcon, - 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - 'active': false, - }, - ]; } diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart new file mode 100644 index 00000000..2a3e486f --- /dev/null +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -0,0 +1,497 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/common/custom_dialog.dart'; +import 'package:syncrow_web/pages/common/hour_picker_dialog.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; +import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart'; +import 'package:syncrow_web/services/access_mang_api.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; + +class VisitorPasswordBloc + extends Bloc { + VisitorPasswordBloc() : super(VisitorPasswordInitial()) { + on(selectUsageFrequency); + on(_onFetchDevice); + on(selectAccessType); + on(selectTimeVisitorPassword); + on(toggleRepeat); + on(toggleDaySelection); + on(selectDevice); + on(_onUpdateFilteredDevices); + on(postOnlineOneTimePassword); + on(postOnlineMultipleTimePassword); + on(postOfflineMultipleTimePassword); + on(postOfflineOneTimePassword); + on(selectTimeOfLinePassword); + on(changeTime); + } + final TextEditingController userNameController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + + final TextEditingController deviceNameController = TextEditingController(); + final TextEditingController deviceIdController = TextEditingController(); + final TextEditingController unitNameController = TextEditingController(); + final TextEditingController virtualAddressController = + TextEditingController(); + List selectedDevices = []; + + List data = []; + List selectedDeviceIds = []; + String effectiveTime = 'Start Time'; + String expirationTime = 'End Time'; + + final forgetFormKey = GlobalKey(); + + String accessTypeSelected = 'Online Password'; + String usageFrequencySelected = 'One-Time'; + String passwordController = ''; + + bool repeat = false; + + int? effectiveTimeTimeStamp; + int? expirationTimeTimeStamp; + + DateTime? startTime = DateTime.now(); + DateTime? endTime; + + String startTimeAccess = 'Start Time'; + String endTimeAccess = 'End Time'; + + selectAccessType( + SelectPasswordType event, Emitter emit) { + accessTypeSelected = event.type; + emit(PasswordTypeSelected(event.type)); + } + + selectUsageFrequency( + SelectUsageFrequency event, Emitter emit) { + usageFrequencySelected = event.usageType; + emit(UsageFrequencySelected(event.usageType)); + } + + Future selectTimeVisitorPassword( + SelectTimeVisitorPassword event, + Emitter emit, + ) async { + final DateTime? picked = await showDatePicker( + context: event.context, + initialDate: DateTime.now(), + firstDate: DateTime(2015, 8), + lastDate: DateTime(3101), + ); + + if (picked != null) { + final TimeOfDay? timePicked = await showTimePicker( + context: event.context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + onSurface: Colors.black, + ), + buttonTheme: const ButtonThemeData( + colorScheme: ColorScheme.light( + primary: Colors.green, + ), + ), + ), + child: child!, + ); + }, + ); + + if (timePicked != null) { + final selectedDateTime = DateTime( + picked.year, + picked.month, + picked.day, + timePicked.hour, + timePicked.minute, + ); + + final selectedTimestamp = + selectedDateTime.millisecondsSinceEpoch ~/ 1000; + + if (event.isStart) { + if (expirationTimeTimeStamp != null && + selectedTimestamp > expirationTimeTimeStamp!) { + CustomSnackBar.displaySnackBar( + 'Effective Time cannot be later than Expiration Time.', + ); + return; + } + effectiveTimeTimeStamp = selectedTimestamp; + startTimeAccess = selectedDateTime.toString().split('.').first; + } else { + if (effectiveTimeTimeStamp != null && + selectedTimestamp < effectiveTimeTimeStamp!) { + CustomSnackBar.displaySnackBar( + 'Expiration Time cannot be earlier than Effective Time.', + ); + return; + } + expirationTimeTimeStamp = selectedTimestamp; + endTimeAccess = selectedDateTime.toString().split('.').first; + } + emit(ChangeTimeState()); + emit(VisitorPasswordInitial()); + } + } + } + + bool toggleRepeat( + ToggleRepeatEvent event, Emitter emit) { + emit(LoadingInitialState()); + repeat = !repeat; + emit(IsRepeatState(repeat: repeat)); + return repeat; + } + + List> days = [ + {"day": "Sun", "key": "Sun"}, + {"day": "Mon", "key": "Mon"}, + {"day": "Tue", "key": "Tue"}, + {"day": "Wed", "key": "Wed"}, + {"day": "Thu", "key": "Thu"}, + {"day": "Fri", "key": "Fri"}, + {"day": "Sat", "key": "Sat"}, + ]; + + List selectedDays = []; + + Future toggleDaySelection( + ToggleDaySelectionEvent event, + Emitter emit, + ) async { + emit(LoadingInitialState()); + if (selectedDays.contains(event.key)) { + selectedDays.remove(event.key); + } else { + selectedDays.add(event.key); + } + emit(ChangeTimeState()); + } + + Future _onFetchDevice( + FetchDevice event, Emitter emit) async { + try { + emit(DeviceLoaded()); + data = await AccessMangApi().fetchDevices(); + emit(TableLoaded(data)); + } catch (e) { + emit(FailedState(e.toString())); + } + } + + //online password + Future postOnlineOneTimePassword(OnlineOneTimePasswordEvent event, + Emitter emit) async { + try { + emit(LoadingInitialState()); + generate7DigitNumber(); + bool res = await AccessMangApi().postOnlineOneTime( + email: event.email, + password: passwordController, + devicesUuid: selectedDevices, + passwordName: event.passwordName, + effectiveTime: effectiveTimeTimeStamp.toString(), + invalidTime: expirationTimeTimeStamp.toString()); + if (res == true) { + emit(SuccessState()); + } else { + throw Exception('Failed to create password'); + } + emit(TableLoaded(data)); + } catch (e) { + emit(FailedState(e.toString())); + Navigator.pop(event.context!); + stateDialog( + context: event.context!, + message: e.toString(), + title: 'Something Wrong'); + } + } + + + Future postOnlineMultipleTimePassword( + OnlineMultipleTimePasswordEvent event, + Emitter emit) async { + try { + emit(LoadingInitialState()); + + await generate7DigitNumber(); + bool res = await AccessMangApi().postOnlineMultipleTime( + scheduleList: [ + if (repeat) + Schedule( + effectiveTime: getTimeFromDateTimeString(expirationTime), + invalidTime: + getTimeFromDateTimeString(effectiveTime).toString(), + workingDay: selectedDays, + ), + ], + password: passwordController, + invalidTime: expirationTimeTimeStamp.toString(), + effectiveTime: effectiveTimeTimeStamp.toString(), + email: event.email, + devicesUuid: selectedDevices, + passwordName: event.passwordName); + if (res == true) { + emit(SuccessState()); + }else { + throw Exception('Failed to create password'); + } + emit(TableLoaded(data)); + + } catch (e) { + emit(FailedState(e.toString())); + Navigator.pop(event.context!); + stateDialog( + context: event.context!, + message: e.toString(), + title: 'Something Wrong'); + } + } + + //offline password + Future postOfflineOneTimePassword(OfflineOneTimePasswordEvent event, + Emitter emit) async { + try { + emit(LoadingInitialState()); + await generate7DigitNumber(); + bool res = await AccessMangApi().postOffLineOneTime( + email: event.email, + devicesUuid: selectedDevices, + passwordName: event.passwordName); + if (res == true) { + emit(SuccessState()); + }else { + throw Exception('Failed to create password'); + } + emit(TableLoaded(data)); + + } catch (e) { + emit(FailedState(e.toString())); + Navigator.pop(event.context!); + stateDialog( + context: event.context!, + message: e.toString(), + title: 'Something Wrong'); + } + } + + Future postOfflineMultipleTimePassword( + OfflineMultipleTimePasswordEvent event, + Emitter emit) async { + try { + emit(LoadingInitialState()); + await generate7DigitNumber(); + bool res = await AccessMangApi().postOffLineMultipleTime( + email: event.email, + devicesUuid: selectedDevices, + passwordName: event.passwordName, + invalidTime: expirationTimeTimeStamp.toString(), + effectiveTime: effectiveTimeTimeStamp.toString(), + ); + if (res == true) { + emit(SuccessState()); + }else { + throw Exception('Failed to create password'); + } + emit(TableLoaded(data)); + + } catch (e) { + emit(FailedState(e.toString())); + Navigator.pop(event.context!); + stateDialog( + context: event.context!, + message: e.toString(), + title: 'Something Wrong'); } + } + + void selectDevice( + SelectDeviceEvent event, Emitter emit) { + if (selectedDeviceIds.contains(event.deviceId)) { + selectedDeviceIds.remove(event.deviceId); + } else { + selectedDeviceIds.add(event.deviceId); + } + } + + String? validate(String? value) { + if (value == null || value.isEmpty) { + return ''; + } + return null; + } + + Future generate7DigitNumber() async { + passwordController = ''; + Random random = Random(); + int min = 1000000; + int max = 9999999; + passwordController = (min + random.nextInt(max - min + 1)).toString(); + return passwordController; + } + + String getTimeOnly(DateTime? dateTime) { + if (dateTime == null) return ''; + return DateFormat('HH:mm').format(dateTime); + } + + void filterDevices() { + final deviceName = deviceNameController.text.toLowerCase(); + final deviceId = deviceIdController.text.toLowerCase(); + final unitName = unitNameController.text.toLowerCase(); + final filteredData = data.where((device) { + final matchesDeviceName = device.name.toLowerCase().contains(deviceName); + final matchesDeviceId = device.uuid.toLowerCase().contains(deviceId); + // final matchesUnitName = device.unitName.toLowerCase().contains(unitName); // Assuming unitName is a property of the device + return matchesDeviceName && matchesDeviceId; + }).toList(); + add(UpdateFilteredDevicesEvent(filteredData)); + } + + @override + Stream mapEventToState( + VisitorPasswordEvent event) async* { + if (event is FetchDevice) { + } else if (event is UpdateFilteredDevicesEvent) { + yield TableLoaded(event.filteredData); + } + } + + void _onUpdateFilteredDevices( + UpdateFilteredDevicesEvent event, Emitter emit) { + emit(TableLoaded(event.filteredData)); + } + + addDeviceToList(context) { + selectedDevices = selectedDeviceIds; + Navigator.of(context).pop(selectedDevices); + } + + Future selectTimeOfLinePassword( + SelectTimeEvent event, Emitter emit) async { + emit(ChangeTimeState()); + final DateTime? picked = await showDatePicker( + context: event.context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(3101), + ); + if (picked != null) { + final TimeOfDay? timePicked = await showHourPicker( + context: event.context, + initialTime: TimeOfDay.now(), + ); + if (timePicked != null) { + final selectedDateTime = DateTime( + picked.year, + picked.month, + picked.day, + timePicked.hour, + 0, + ); + final selectedTimestamp = DateTime( + selectedDateTime.year, + selectedDateTime.month, + selectedDateTime.day, + selectedDateTime.hour, + selectedDateTime.minute, + ).millisecondsSinceEpoch ~/ + 1000; // Divide by 1000 to remove milliseconds + if (event.isEffective) { + if (expirationTimeTimeStamp != null && + selectedTimestamp > expirationTimeTimeStamp!) { + CustomSnackBar.displaySnackBar( + 'Effective Time cannot be later than Expiration Time.'); + } else { + effectiveTime = selectedDateTime + .toString() + .split('.') + .first; // Remove seconds and milliseconds + effectiveTimeTimeStamp = selectedTimestamp; + } + } else { + if (effectiveTimeTimeStamp != null && + selectedTimestamp < effectiveTimeTimeStamp!) { + CustomSnackBar.displaySnackBar( + 'Expiration Time cannot be earlier than Effective Time.'); + } else { + expirationTime = selectedDateTime + .toString() + .split('.') + .first; // Remove seconds and milliseconds + expirationTimeTimeStamp = selectedTimestamp; + } + } + emit(TimeSelectedState()); + } + } + } + + changeTime(ChangeTimeEvent event, Emitter emit) { + if (event.isStartEndTime == true) { + startTime = event.val; + } else { + endTime = event.val; + } + } + + DateTime? convertStringToDateTime(String dateTimeString) { + try { + final DateFormat inputFormat = DateFormat('yyyy-MM-dd HH:mm:ss'); + DateTime dateTime = inputFormat.parse(dateTimeString); + return dateTime; + } catch (e) { + print("Error parsing date: $e"); + return null; + } + } + + String getTimeFromDateTimeString(String dateTimeString) { + DateTime? dateTime = convertStringToDateTime(dateTimeString); + if (dateTime == null) return ''; + return DateFormat('HH:mm').format(dateTime); + } + + String? validateEmail(String? value) { + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value!)) { + return ''; + } + return null; + } + + Future stateDialog({ + BuildContext? context, + String? message, + String? title, + dynamic actions, + }) { + return showCustomDialog( + context: context!, + message: message!, + iconPath: Assets.deviceNoteIcon, + title: title, + dialogHeight: 150, + actions: actions ?? + [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + } +} diff --git a/lib/pages/visitor_password/bloc/visitor_password_event.dart b/lib/pages/visitor_password/bloc/visitor_password_event.dart new file mode 100644 index 00000000..9526bf54 --- /dev/null +++ b/lib/pages/visitor_password/bloc/visitor_password_event.dart @@ -0,0 +1,137 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; + +abstract class VisitorPasswordEvent extends Equatable { + const VisitorPasswordEvent(); + + @override + List get props => []; +} + +class SelectPasswordType extends VisitorPasswordEvent { + final String type; + + const SelectPasswordType(this.type); + + @override + List get props => [type]; +} + +class SelectUsageFrequency extends VisitorPasswordEvent { + final String usageType; + + const SelectUsageFrequency(this.usageType); + + @override + List get props => [usageType]; +} +class SelectTimeVisitorPassword extends VisitorPasswordEvent { + final BuildContext context; + final bool isStart; + final bool isRepeat; + + const SelectTimeVisitorPassword({ required this.context,required this.isStart,required this.isRepeat}); + + @override + List get props => [context,isStart,isRepeat]; +} + + + +class ToggleDaySelectionEvent extends VisitorPasswordEvent { + final String key; + + const ToggleDaySelectionEvent({required this.key}); + @override + List get props => [key]; +} + + +class ToggleRepeatEvent extends VisitorPasswordEvent {} +class GeneratePasswordEvent extends VisitorPasswordEvent {} + +class FetchDevice extends VisitorPasswordEvent { +} + +//online password +class OnlineOneTimePasswordEvent extends VisitorPasswordEvent { + final String? email; + final String? passwordName; + final BuildContext? context; + + const OnlineOneTimePasswordEvent({this.email,this.passwordName,this.context}); + + @override + List get props => [email!,passwordName!,]; +} +class OnlineMultipleTimePasswordEvent extends VisitorPasswordEvent { + final String? email; + final String? passwordName; + final String? invalidTime; + final String? effectiveTime; + final BuildContext? context; + const OnlineMultipleTimePasswordEvent({this.email,this.passwordName,this.invalidTime,this.effectiveTime,this.context}); + @override + List get props => [email!,passwordName!,invalidTime!,effectiveTime!,context!]; +} + +//offline password +class OfflineOneTimePasswordEvent extends VisitorPasswordEvent { + final BuildContext? context; + final String? email; + final String? passwordName; + const OfflineOneTimePasswordEvent({this.email,this.passwordName,this.context}); + @override + List get props => [email!,passwordName!,context!,]; +} + +class OfflineMultipleTimePasswordEvent extends VisitorPasswordEvent { + final String? email; + final String? passwordName; + final String? invalidTime; + final String? effectiveTime; + final BuildContext? context; + + const OfflineMultipleTimePasswordEvent({this.context,this.email,this.passwordName,this.invalidTime,this.effectiveTime}); + + @override + List get props => [email!,passwordName!,invalidTime!,effectiveTime!,context!]; +} + + +class SelectDeviceEvent extends VisitorPasswordEvent { + final String deviceId; + const SelectDeviceEvent(this.deviceId); +} + +class FilterDataEvent extends VisitorPasswordEvent { + final String? passwordName; + final int? startTime; + final int? endTime; + + const FilterDataEvent({ + this.passwordName, + this.startTime, + this.endTime, + }); +} +class UpdateFilteredDevicesEvent extends VisitorPasswordEvent { + final List filteredData; + + UpdateFilteredDevicesEvent(this.filteredData); +}class SelectTimeEvent extends VisitorPasswordEvent { + final BuildContext context; + final bool isEffective; + const SelectTimeEvent({required this.context,required this.isEffective}); + @override + List get props => [context,isEffective]; +} +class ChangeTimeEvent extends VisitorPasswordEvent { + final dynamic val; + final bool isStartEndTime; + + const ChangeTimeEvent({required this.val,required this.isStartEndTime}); + @override + List get props => [val,isStartEndTime]; +} \ No newline at end of file diff --git a/lib/pages/visitor_password/bloc/visitor_password_state.dart b/lib/pages/visitor_password/bloc/visitor_password_state.dart new file mode 100644 index 00000000..279c9809 --- /dev/null +++ b/lib/pages/visitor_password/bloc/visitor_password_state.dart @@ -0,0 +1,65 @@ + + +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; + +abstract class VisitorPasswordState extends Equatable { + const VisitorPasswordState(); + + @override + List get props => []; +} + +class VisitorPasswordInitial extends VisitorPasswordState {} + + + +class PasswordTypeSelected extends VisitorPasswordState { + final String selectedType; + const PasswordTypeSelected(this.selectedType); + @override + List get props => [selectedType]; +} + +class UsageFrequencySelected extends VisitorPasswordState { + final String selectedFrequency; + + const UsageFrequencySelected(this.selectedFrequency); + + @override + List get props => [selectedFrequency]; +} + +class IsRepeatState extends VisitorPasswordState { + final bool repeat; + const IsRepeatState({required this.repeat}); + + @override + List get props => [repeat]; + +} + +class LoadingInitialState extends VisitorPasswordState {} +class ChangeTimeState extends VisitorPasswordState {} +class TimeSelectedState extends VisitorPasswordState {} +class DeviceLoaded extends VisitorPasswordState {} +class SuccessState extends VisitorPasswordState {} + +class FailedState extends VisitorPasswordState { + final String message; + const FailedState(this.message); + @override + List get props => [message]; +} + +class TableLoaded extends VisitorPasswordState { + final List data; + const TableLoaded(this.data); + @override + List get props => [data]; +} + +class DeviceSelectionUpdated extends VisitorPasswordState { + final List selectedDeviceIds; + const DeviceSelectionUpdated(this.selectedDeviceIds); +} \ No newline at end of file diff --git a/lib/pages/visitor_password/model/device_model.dart b/lib/pages/visitor_password/model/device_model.dart new file mode 100644 index 00000000..2c3ce8d9 --- /dev/null +++ b/lib/pages/visitor_password/model/device_model.dart @@ -0,0 +1,101 @@ + + +import 'package:syncrow_web/utils/constants/const.dart'; + +class DeviceModel { + dynamic productUuid; + dynamic productType; + dynamic activeTime; + dynamic category; + dynamic categoryName; + dynamic createTime; + dynamic gatewayId; + dynamic icon; + dynamic ip; + dynamic lat; + dynamic localKey; + dynamic lon; + dynamic model; + dynamic name; + DeviseStatus online; + dynamic ownerId; + dynamic sub; + dynamic timeZone; + dynamic updateTime; + dynamic uuid; + + DeviceModel({ + required this.productUuid, + required this.productType, + required this.activeTime, + required this.category, + required this.categoryName, + required this.createTime, + required this.gatewayId, + required this.icon, + required this.ip, + required this.lat, + required this.localKey, + required this.lon, + required this.model, + required this.name, + required this.online, + required this.ownerId, + required this.sub, + required this.timeZone, + required this.updateTime, + required this.uuid, + }); + + // Deserialize from JSON + factory DeviceModel.fromJson(Map json) { + return DeviceModel( + productUuid: json['productUuid'] , + productType: json['productType'], + activeTime: json['activeTime'], + category: json['category'] , + categoryName: json['categoryName'] , + createTime: json['createTime'] , + gatewayId: json['gatewayId'], + icon: json['icon'], + ip: json['ip'] , + lat: json['lat'] , + localKey: json['localKey'] , + lon: json['lon'] , + model: json['model'] , + name: json['name'], + online: OnlineTypeExtension.fromString(json['online']), + ownerId: json['ownerId'] , + sub: json['sub'], + timeZone: json['timeZone'], + updateTime: json['updateTime'] , + uuid: json['uuid'], + ); + } + + // Serialize to JSON + Map toJson() { + return { + 'productUuid': productUuid, + 'productType': productType, + 'activeTime': activeTime, + 'category': category, + 'categoryName': categoryName, + 'createTime': createTime, + 'gatewayId': gatewayId, + 'icon': icon, + 'ip': ip, + 'lat': lat, + 'localKey': localKey, + 'lon': lon, + 'model': model, + 'name': name, + 'online': online, + 'ownerId': ownerId, + 'sub': sub, + 'timeZone': timeZone, + 'updateTime': updateTime, + 'uuid': uuid, + }; + } +} diff --git a/lib/pages/visitor_password/model/schedule_model.dart b/lib/pages/visitor_password/model/schedule_model.dart new file mode 100644 index 00000000..efeeff35 --- /dev/null +++ b/lib/pages/visitor_password/model/schedule_model.dart @@ -0,0 +1,27 @@ +class Schedule { + final String effectiveTime; + final String invalidTime; + final List workingDay; + + Schedule({ + required this.effectiveTime, + required this.invalidTime, + required this.workingDay, + }); + + factory Schedule.fromJson(Map json) { + return Schedule( + effectiveTime: json['effectiveTime'], + invalidTime: json['invalidTime'], + workingDay: List.from(json['workingDay']), + ); + } + + Map toJson() { + return { + 'effectiveTime': effectiveTime, + 'invalidTime': invalidTime, + 'workingDay': workingDay, + }; + } +} \ No newline at end of file diff --git a/lib/pages/visitor_password/view/add_device_dialog.dart b/lib/pages/visitor_password/view/add_device_dialog.dart new file mode 100644 index 00000000..e5e4853c --- /dev/null +++ b/lib/pages/visitor_password/view/add_device_dialog.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/common/custom_table.dart'; +import 'package:syncrow_web/pages/common/custom_web_textfield.dart'; +import 'package:syncrow_web/pages/common/default_button.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/constants/const.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AddDeviceDialog extends StatelessWidget { + final List? selectedDeviceIds; + const AddDeviceDialog({super.key,this.selectedDeviceIds }); + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return BlocProvider( + create: (context) => VisitorPasswordBloc()..add(FetchDevice()), + child: BlocBuilder( + builder: (BuildContext context, VisitorPasswordState state) { + final visitorBloc = BlocProvider.of(context); + if (state is TableLoaded) { + for (var device in selectedDeviceIds!) { + if (selectedDeviceIds!.contains(device)) { + visitorBloc.add(SelectDeviceEvent(device)); + } + } + } + + return AlertDialog( + backgroundColor: Colors.white, + title: Text('Add Accessible Device', + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 24, + color: Colors.black),), + content: Container( + height: MediaQuery.of(context).size.height/1.7, + width: MediaQuery.of(context).size.width/2, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Container( + width: size.width, + padding: EdgeInsets.all(15), + decoration:containerDecoration.copyWith( + color: ColorsManager.worningColor, + border: Border.all(color: Color(0xffFFD22F)), + boxShadow: [] + ), + child: Row( + children: [ + SizedBox( + child: SvgPicture.asset( + Assets.deviceNoteIcon, + height: 15, + width: 15, + ), + ), + const SizedBox(width: 10,), + Text('Only online accessible devices can be added', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.grayColor),), + ], + ) + ), + const SizedBox(height: 20,), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.alphabetic, + + children: [ + Expanded( + flex: 4, + child: CustomWebTextField( + controller: visitorBloc.deviceNameController, + isRequired: true, + textFieldName: 'Device Name', + description: '', + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 4, + child: CustomWebTextField( + controller: visitorBloc.deviceIdController, + isRequired: true, + textFieldName: 'Device ID', + description: '', + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 4, + child: CustomWebTextField( + controller: visitorBloc.unitNameController, + isRequired: true, + textFieldName: 'Unit Name', + description: '', + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 2, + child: Container( + child: SizedBox( + width: size.width * 0.06, + child: Center( + child: DefaultButton( + onPressed: () { + visitorBloc.filterDevices(); + }, + borderRadius: 9, + child: const Text('Search'), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 2, + child: Container( + width: size.width * 0.06, + child: DefaultButton( + backgroundColor: ColorsManager.whiteColors, + borderRadius: 9, + child: Text('Reset', + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black), + ), + onPressed: () { + visitorBloc.deviceNameController.clear(); + visitorBloc.deviceIdController.clear(); + visitorBloc.unitNameController.clear(); + visitorBloc.add(FetchDevice()); // Reset to original list + }, + ), + ), + ) + ], + ), + const SizedBox(height: 20), + Expanded( + flex: 3, + child: state is TableLoaded + ? DynamicTable( + initialSelectedIds:selectedDeviceIds , + cellDecoration: containerDecoration, + isEmpty:visitorBloc.data.isEmpty, + selectAll: (p0) { + visitorBloc.selectedDeviceIds.clear(); + for (var item in state.data) { + visitorBloc.add(SelectDeviceEvent(item.uuid)); + } + }, + onRowCheckboxChanged: (index, isSelected) { + final deviceId = state.data[index].uuid; + visitorBloc.add(SelectDeviceEvent(deviceId)); + }, + withCheckBox: true, + size: size*0.5, + headers: const [ 'Device Name', 'Device ID', 'Access Type', 'Unit Name', 'Status'], + data: state.data.map((item) { + return [ + item.name.toString(), + item.uuid.toString(), + item.productType.toString(), + '', + item.online.value.toString(), + ]; + }).toList(), + ) + : const Center(child: CircularProgressIndicator())) + ], + ), + ), + ), + actionsAlignment: MainAxisAlignment.center, + actions: [ + Container( + decoration: containerDecoration, + width: size.width * 0.2, + child: DefaultButton( + borderRadius: 8, + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + backgroundColor: Colors.white, + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodyMedium!, + ), + ), + ), + Container( + decoration: containerDecoration, + width: size.width * 0.2, + child: DefaultButton( + onPressed: () { + visitorBloc.addDeviceToList(context); + }, + borderRadius: 8, + child: Text('Ok'), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/repeat_widget.dart b/lib/pages/visitor_password/view/repeat_widget.dart new file mode 100644 index 00000000..ae37b1e3 --- /dev/null +++ b/lib/pages/visitor_password/view/repeat_widget.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/date_time_widget.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class RepeatWidget extends StatelessWidget { + const RepeatWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return BlocBuilder( + builder: (context, state) { + final visitorBloc = BlocProvider.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Wrap the Row in a SingleChildScrollView to handle overflow + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: visitorBloc.days.map((day) { + return Container( + width: 70, // Adjust width as needed + margin: EdgeInsets.all(5), + child: CheckboxListTile( + contentPadding: EdgeInsets.zero, + title: Text( + day['day']!, + style: TextStyle( + fontSize: 10, + color: visitorBloc.selectedDays.contains(day['key']) + ? Colors.black + : ColorsManager.blackColor, + ), + ), + value: visitorBloc.selectedDays.contains(day['key']), + onChanged: (bool? value) { + if (value != null) { + visitorBloc.add(ToggleDaySelectionEvent(key: day['key']!)); + } + }, + ), + ); + }).toList(), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: DateTimeWebWidget( + icon: Assets.timeIcon, + isRequired: false, + title: '', + size: size, + endTime: () { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: false)); + Future.delayed(const Duration(milliseconds: 500), () { + visitorBloc.add(ChangeTimeEvent(val: visitorBloc.endTime, isStartEndTime: true)); + }); + }, + startTime: () { + Future.delayed(const Duration(milliseconds: 500), () { + visitorBloc.add(ChangeTimeEvent(val: visitorBloc.endTime, isStartEndTime: true)); + }); + visitorBloc.add(SelectTimeEvent(context: context, isEffective: true)); + }, + firstString: visitorBloc.effectiveTime, + secondString: visitorBloc.expirationTime, + ), + ), + const SizedBox(height: 20), + ], + ); + } + ); + } +} diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart new file mode 100644 index 00000000..ff3d5a04 --- /dev/null +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -0,0 +1,538 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/common/custom_web_textfield.dart'; +import 'package:syncrow_web/pages/common/date_time_widget.dart'; +import 'package:syncrow_web/pages/common/default_button.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart'; +import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class VisitorPasswordDialog extends StatelessWidget { + const VisitorPasswordDialog({super.key}); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + var text = Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black,fontSize: 13); + return BlocProvider( + create: (context) => VisitorPasswordBloc(), + child: BlocListener( + listener: (context, state) { + final visitorBloc = BlocProvider.of(context); + if (state is SuccessState) { + visitorBloc.stateDialog( + context: context, + message: 'Password Created Successfully', + title: 'Send Success', + ); + } else if (state is FailedState) { + visitorBloc.stateDialog( + context: context, + message: state.message, + title: 'Something Wrong', + ); + } + }, + child: BlocBuilder( + builder: (BuildContext context, VisitorPasswordState state) { + final visitorBloc = BlocProvider.of(context); + bool isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; + return AlertDialog( + backgroundColor: Colors.white, + title: Text( + 'Create visitor password', + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 24, + color: Colors.black), + ), + content: + state is LoadingInitialState ?const Center(child: CircularProgressIndicator()): + SingleChildScrollView( + child: Form( + key: visitorBloc.forgetFormKey, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: ListBody( + children: [ + Container( + child: Row( + children: [ + Expanded( + flex: 2, + child: CustomWebTextField( + validator: visitorBloc.validate, + controller: visitorBloc.userNameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: CustomWebTextField( + validator: visitorBloc.validateEmail, + controller: visitorBloc.emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ), + const Spacer(), + ], + ), + ), + const SizedBox( + height: 15, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('* ', + style: Theme.of(context).textTheme + .bodyMedium!.copyWith(color: Colors.red), + ), + Text('Access Type', + style:text ), + ], + + ), + Row( + children: [ + Flexible( + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text('Online Password', + style: text, + ), + value: 'Online Password', + groupValue: (state is PasswordTypeSelected) + ? state.selectedType + : visitorBloc.accessTypeSelected, + onChanged: (String? value) { + if (value != null) { + context.read() + .add(SelectPasswordType(value)); + } + }, + ), + ), + Flexible( + child: RadioListTile( + contentPadding: EdgeInsets.zero, + + title: Text('Offline Password', + style:text ), + value: 'Offline Password', + groupValue: (state is PasswordTypeSelected) + ? state.selectedType + : visitorBloc.accessTypeSelected, + onChanged: (String? value) { + if (value != null) { + context.read().add(SelectPasswordType(value)); + } + }, + ), + ), + Flexible( + child: RadioListTile( + contentPadding: EdgeInsets.zero, + + title: Text('Dynamic Password', + style: text,), + value: 'Dynamic Password', + groupValue: (state is PasswordTypeSelected) + ? state.selectedType + : visitorBloc.accessTypeSelected, + onChanged: (String? value) { + if (value != null) { + context.read() + .add(SelectPasswordType(value)); + visitorBloc.usageFrequencySelected = ''; + } + }, + ), + ), + ], + ), + Text( + 'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor,fontSize: 9),), + const SizedBox( + height: 20, + ) + ], + ), + visitorBloc.accessTypeSelected == 'Dynamic Password' + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('* ', + style: Theme.of(context).textTheme.bodyMedium! + .copyWith(color: Colors.red), + ), + Text('Usage Frequency',style:text ,), + ], + ), + Row( + children: [ + Flexible( + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text('One-Time', + style:text ,), + value: 'One-Time', + groupValue: + (state is UsageFrequencySelected) + ? state.selectedFrequency + : visitorBloc.usageFrequencySelected, + onChanged: (String? value) { + if (value != null) { + context.read() + .add(SelectUsageFrequency(value)); + } + }, + ), + ), + Flexible( + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text('Periodic', + style: text), + value: 'Periodic', + groupValue: (state is UsageFrequencySelected) + ? state.selectedFrequency + : visitorBloc.usageFrequencySelected, + onChanged: (String? value) { + if (value != null) { + context.read() + .add(SelectUsageFrequency(value)); + } + }, + ), + ), + ], + ), + Text('Within the validity period, each device can be unlocked only once.', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor,fontSize: 9), + ) + ], + ), + const SizedBox( + height: 20, + ), + if ((visitorBloc.usageFrequencySelected != 'One-Time' || + visitorBloc.accessTypeSelected != 'Offline Password') && + (visitorBloc.usageFrequencySelected != '')) + DateTimeWebWidget( + isRequired: true, + title: 'Access Period', + size: size, + endTime: () { + visitorBloc.add(SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); + }, + startTime: () { + visitorBloc.add(SelectTimeVisitorPassword( + context: context, + isStart: true, + isRepeat: false)); + }, + firstString: visitorBloc.startTimeAccess.toString(), + secondString: visitorBloc.endTimeAccess.toString(), + icon: Assets.calendarIcon + ), + const SizedBox( + height: 20, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('* ', + style: Theme.of(context).textTheme.bodyMedium! + .copyWith(color: Colors.red), + ), + Text('Access Devices', + style:text ,), + ], + ), + Text( + 'Within the validity period, each device can be unlocked only once.', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor,fontSize: 9),), + const SizedBox( + height: 20, + ), + if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Online Password') + SizedBox( + width: 100, + child: Column( + children: [ + Text('Repeat', + style:text), + Transform.scale( + scale: .8, + child: CupertinoSwitch( + value: visitorBloc.repeat, + onChanged: (value) { + visitorBloc.add(ToggleRepeatEvent()); + }, + applyTheme: true, + ), + ), + ], + + ), + ), + if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Online Password') + isRepeat ? const RepeatWidget() : const SizedBox(), + Container( + decoration: containerDecoration, + width: size.width / 9, + child: DefaultButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AddDeviceDialog(selectedDeviceIds: visitorBloc.selectedDevices,); + }, + ).then((listDevice) { + if(listDevice!=null){ + visitorBloc.selectedDevices = listDevice; + } + }); + }, + borderRadius: 8, + child: Text('+ Add Device',style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.whiteColors,fontSize: 12),), + ), + ), + ], + ), + ], + ), + ), + ), + ), + actionsAlignment: MainAxisAlignment.center, + actions: [ + Container( + decoration: containerDecoration, + width: size.width * 0.2, + child: DefaultButton( + borderRadius: 8, + onPressed: () { + Navigator.of(context).pop(true); + }, + backgroundColor: Colors.white, + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor,fontSize: 16), + ), + ), + ), + Container( + decoration: containerDecoration, + width: size.width * 0.2, + child: DefaultButton( + onPressed: () { + if (visitorBloc.forgetFormKey.currentState!.validate()) { + if(visitorBloc.selectedDevices.isNotEmpty){ + if(visitorBloc.effectiveTimeTimeStamp!=null&&visitorBloc.expirationTimeTimeStamp!=null) { + setPasswordFunction(context, size, visitorBloc); + } + else{ + visitorBloc.stateDialog(context: + context,message: 'Please select Access Period to continue',title: 'Access Period'); + } + }else{ + visitorBloc.stateDialog(context: + context,message: 'Please select devices to continue',title: 'Select Devices'); + } + } + }, + borderRadius: 8, + child: Text('Ok', style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.whiteColors,fontSize: 16),), + ), + ), + ], + ); }, + ), + ), + + ); + } + + Future setPasswordFunction( + BuildContext context, + Size size, + VisitorPasswordBloc visitorBloc, + ) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingInitialState) { + // Show loading indicator while loading + return AlertDialog( + alignment: Alignment.center, + content: SizedBox( + height: size.height * 0.25, + child: Center( + child: CircularProgressIndicator(), // Display a loading spinner + ), + ), + ); + }else{ + return AlertDialog( + alignment: Alignment.center, + content: SizedBox( + height: size.height * 0.25, + child: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + child: SvgPicture.asset( + Assets.deviceNoteIcon, + height: 35, + width: 35, + ), + ), + Text( + 'Set Password', + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontSize: 30, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + ), + ], + ), + const SizedBox(width: 15), + Text( + 'This action will update all of the selected\n door locks passwords in the property.\n\nAre you sure you want to continue?', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + fontSize: 18, + ), + ), + ], + ), + ), + actionsAlignment: MainAxisAlignment.center, + actions: [ + Container( + decoration: containerDecoration, + width: size.width * 0.1, + child: DefaultButton( + borderRadius: 8, + onPressed: () { + Navigator.of(context).pop(); + }, + backgroundColor: Colors.white, + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + fontSize: 16, + ), + ), + ), + ), + Container( + decoration: containerDecoration, + width: size.width * 0.1, + child: DefaultButton( + borderRadius: 8, + onPressed: () { + Navigator.pop(context); + if (visitorBloc.usageFrequencySelected == 'One-Time' && + visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.add(OnlineOneTimePasswordEvent( + context: context, + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + )); + } else if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.add(OnlineMultipleTimePasswordEvent( + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + )); + } else if (visitorBloc.usageFrequencySelected == 'One-Time' && + visitorBloc.accessTypeSelected == 'Offline Password') { + visitorBloc.add(OfflineOneTimePasswordEvent( + context: context, + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + )); + } else if (visitorBloc.usageFrequencySelected == 'Periodic' && + visitorBloc.accessTypeSelected == 'Offline Password') { + visitorBloc.add(OfflineMultipleTimePasswordEvent( + passwordName: visitorBloc.userNameController.text, + email: visitorBloc.emailController.text, + effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + )); + } + }, + child: Text( + 'Ok', + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.whiteColors, + fontSize: 16, + ), + ), + ), + ), + ], + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/services/access_mang_api.dart b/lib/services/access_mang_api.dart new file mode 100644 index 00000000..6a2cf40d --- /dev/null +++ b/lib/services/access_mang_api.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; +import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class AccessMangApi{ + + Future> fetchVisitorPassword() async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.visitorPassword, + showServerMessage: true, + expectedResponseModel: (json) { + List jsonData = json; + List passwordList = jsonData.map((jsonItem) { + return PasswordModel.fromJson(jsonItem); + }).toList(); + return passwordList; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching visitor passwords: $e'); + return []; + } + } + + Future fetchDevices() async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.getDevices, + showServerMessage: true, + expectedResponseModel: (json) { + List jsonData = json; + List passwordList = jsonData.map((jsonItem) { + return DeviceModel.fromJson(jsonItem); + }).toList(); + return passwordList; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return []; + } + } + + Future postOnlineOneTime({ + String? email, + String? passwordName, + String? password, + String? effectiveTime, + String? invalidTime, + List? devicesUuid}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.sendOnlineOneTime, + body: jsonEncode({ + "email": email, + "passwordName": passwordName, + "password": password, + "devicesUuid": devicesUuid, + "effectiveTime":effectiveTime , + "invalidTime": invalidTime + }), + showServerMessage: true, + expectedResponseModel: (json) { + if(json['statusCode'].toString()=='201'){ + return true; + }else{ + return false; + } + }, + ); + return response; + } on DioException catch (e) { + debugPrint('Error: ${e.message}'); + debugPrint('Error fetching ${e.response!.statusMessage}'); + return false; + } + } + + Future postOnlineMultipleTime({ + String? effectiveTime, + String? invalidTime, + String? email, + String? password, + String? passwordName, + List? scheduleList, + List? devicesUuid}) async { + try { + Map body = { + "email": email, + "devicesUuid": devicesUuid, + "passwordName": passwordName, + "password": password, + "effectiveTime": effectiveTime, + "invalidTime": invalidTime, + }; + if (scheduleList != null) { + body["scheduleList"] = scheduleList.map((schedule) => schedule.toJson()).toList(); + } + final response = await HTTPService().post( + path: ApiEndpoints.sendOnlineMultipleTime, + body: jsonEncode(body), + showServerMessage: true, + expectedResponseModel: (json) { + if(json['data']['successOperations'][0]['success'].toString()=='true'){ + return true; + }else{ + return false; + } + }, + ); + return response; + } on DioException catch (e){ + debugPrint('Error fetching ${e.type.name}'); + debugPrint('Error fetching ${e.response!.statusMessage}'); + return false; + } + } + +// OffLine One Time Password + + Future postOffLineOneTime({String? email,String? passwordName,List? devicesUuid}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.sendOffLineOneTime, + body: jsonEncode({ + "email": email, + "passwordName": passwordName, + "devicesUuid": devicesUuid + }), + showServerMessage: true, + expectedResponseModel: (json) { + if (json['data']['successOperations'][0]['success'].toString() == + 'true') { + return true; + } else { + return false; + } + } + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return []; + } + } + + Future postOffLineMultipleTime({ + String? email, + String? passwordName, + String? effectiveTime, + String? invalidTime, + List? devicesUuid + + }) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.sendOffLineOneTime, + body: jsonEncode({ + "email": email, + "devicesUuid":devicesUuid, + "passwordName": passwordName, + "effectiveTime": effectiveTime, + "invalidTime": invalidTime + }), + showServerMessage: true, + expectedResponseModel: (json) { + if (json['data']['successOperations'][0]['success'].toString() == + 'true') { + return true; + } else { + return false; + } + } + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return []; + } + } +} \ No newline at end of file diff --git a/lib/services/api/http_interceptor.dart b/lib/services/api/http_interceptor.dart index ff3fc0d4..cece39a7 100644 --- a/lib/services/api/http_interceptor.dart +++ b/lib/services/api/http_interceptor.dart @@ -13,7 +13,8 @@ class HTTPInterceptor extends InterceptorsWrapper { List headerExclusionListOfAddedParameters = [ ApiEndpoints.login, - ApiEndpoints.getRegion + ApiEndpoints.getRegion, + ApiEndpoints.sendOtp ]; @override diff --git a/lib/services/auth_api.dart b/lib/services/auth_api.dart index 58a705ff..3bb8f7e7 100644 --- a/lib/services/auth_api.dart +++ b/lib/services/auth_api.dart @@ -1,12 +1,12 @@ -import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/token.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; class AuthenticationAPI { - static Future loginWithEmail({required var model}) async { final response = await HTTPService().post( path: ApiEndpoints.login, @@ -19,38 +19,85 @@ class AuthenticationAPI { } static Future forgetPassword( - {required var email, required var password}) async { + {required var email, required var password,}) async { final response = await HTTPService().post( path: ApiEndpoints.forgetPassword, - body: {"email": email, "password": password}, + body: { + "email": email, + "password": password + }, showServerMessage: true, expectedResponseModel: (json) {}); return response; } - static Future sendOtp({required var email}) async { - final response = await HTTPService().post( - path: ApiEndpoints.sendOtp, - body: {"email": email, "type": "VERIFICATION"}, - showServerMessage: true, - expectedResponseModel: (json) {}); - return response; - } - static Future verifyOtp( - {required String email, required String otpCode}) async { - final response = await HTTPService().post( - path: ApiEndpoints.verifyOtp, - body: {"email": email, "type": "VERIFICATION", "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) { - if (json['message'] == 'Otp Verified Successfully') { - return true; - } else { - return false; + static Future sendOtp({required String email, required String regionUuid}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.sendOtp, + body: { + "email": email, + "type": "PASSWORD", + "regionUuid": regionUuid + }, + showServerMessage: true, + expectedResponseModel: (json) { + return 30; } - }); - return response; + ); + return 30; + } on DioException catch (e) { + if (e.response != null) { + if (e.response!.statusCode == 400) { + final errorData = e.response!.data; + String errorMessage = errorData['message']; + if(errorMessage=='User not found'){ + return 1; + }else{ + int cooldown = errorData['data']['cooldown'] ?? 1; + return cooldown; + } + } else { + debugPrint('Error: ${e.response!.statusCode} - ${e.response!.statusMessage}'); + return 1; + } + } else { + debugPrint('Error: ${e.message}'); + return 1; + } + } catch (e) { + debugPrint('Unexpected Error: $e'); + return 1; + } + } + + static Future verifyOtp( + {required String email, required String otpCode}) async { + try{ + final response = await HTTPService().post( + path: ApiEndpoints.verifyOtp, + body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) { + if (json['message'] == 'Otp Verified Successfully') { + return true; + } else { + return false; + } + }); + return response; + }on DioException catch (e){ + if (e.response != null) { + if (e.response!.statusCode == 400) { + final errorData = e.response!.data; + String errorMessage = errorData['message']; + return errorMessage; + } + } else { + debugPrint('Error: ${e.message}'); + } + } } static Future> fetchRegion() async { diff --git a/lib/services/home_api.dart b/lib/services/home_api.dart new file mode 100644 index 00000000..42e732b4 --- /dev/null +++ b/lib/services/home_api.dart @@ -0,0 +1,16 @@ + import 'package:syncrow_web/pages/auth/model/user_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class HomeApi{ + Future fetchUserInfo(userId) async { + final response = await HTTPService().get( + path: ApiEndpoints.getUser.replaceAll('{userUuid}', userId!), + showServerMessage: true, + expectedResponseModel: (json) { + return UserModel.fromJson(json); + } + ); + return response; + } + } \ No newline at end of file diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 46cb4383..2ee12718 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -18,15 +18,18 @@ abstract class ColorsManager { static const Color dozeColor = Color(0xFFFEC258); static const Color relaxColor = Color(0xFFFBD288); static const Color readingColor = Color(0xFFF7D69C); + static const Color worningColor = Color(0xFFFFF3C8); static const Color energizingColor = Color(0xFFEDEDED); static const Color dividerColor = Color(0xFFEBEBEB); static const Color slidingBlueColor = Color(0x99023DFE); static const Color blackColor = Color(0xFF000000); static const Color lightGreen = Color(0xFF00FF0A); static const Color grayColor = Color(0xFF999999); - static const Color red = Colors.red; + static const Color red = Color(0xFFFF0000); static const Color graysColor = Color(0xffEBEBEB); static const Color textGray = Color(0xffD5D5D5); static const Color btnColor = Color(0xFF00008B); static const Color blueColor = Color(0xFF0036E6); + static const Color boxColor = Color(0xFFF5F6F7); + static const Color boxDivider = Color(0xFFE0E0E0); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 855d74e5..f9581cb6 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -9,4 +9,17 @@ abstract class ApiEndpoints { static const String sendOtp = '$baseUrl/authentication/user/send-otp'; static const String verifyOtp = '$baseUrl/authentication/user/verify-otp'; static const String getRegion = '$baseUrl/region'; + static const String visitorPassword = '$baseUrl/visitor-password'; + static const String getDevices = '$baseUrl/visitor-password/devices'; + + + static const String sendOnlineOneTime = '$baseUrl/visitor-password/temporary-password/online/one-time'; + static const String sendOnlineMultipleTime = '$baseUrl/visitor-password/temporary-password/online/multiple-time'; + +//offline Password + static const String sendOffLineOneTime = '$baseUrl/visitor-password/temporary-password/offline/one-time'; + static const String sendOffLineMultipleTime = '$baseUrl/visitor-password/temporary-password/offline/multiple-time'; + + + static const String getUser = '$baseUrl/user/{userUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 0d0b3924..f4a2859e 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -23,4 +23,8 @@ class Assets { static const String energyIcon = "assets/images/energy_icon.svg"; static const String integrationsIcon = "assets/images/Integrations_icon.svg"; static const String assetIcon = "assets/images/asset_icon.svg"; + static const String calendarIcon = "assets/images/calendar_icon.svg"; + static const String deviceNoteIcon = "assets/images/device_note.svg"; + static const String timeIcon = "assets/images/time_icon.svg"; + static const String emptyTable = "assets/images/empty_table.svg"; } diff --git a/lib/utils/constants/const.dart b/lib/utils/constants/const.dart new file mode 100644 index 00000000..77beff69 --- /dev/null +++ b/lib/utils/constants/const.dart @@ -0,0 +1,109 @@ + +enum AccessType { + onlineOnetime, + onlineMultiple, + offlineOnetime, + offlineMultiple, +} + +extension AccessTypeExtension on AccessType { + String get value { + switch (this) { + case AccessType.onlineOnetime: + return "Online Password"; + case AccessType.onlineMultiple: + return "online Multiple Password"; + case AccessType.offlineOnetime: + return "Offline Onetime Password"; + case AccessType.offlineMultiple: + return "Offline Multiple Password"; + } + } + + static AccessType fromString(String value) { + switch (value) { + case "ONLINE_ONETIME": + return AccessType.onlineOnetime; + case "ONLINE_MULTIPLE": + return AccessType.onlineMultiple; + case "OFFLINE_ONETIME": + return AccessType.offlineOnetime; + case "OFFLINE_MULTIPLE": + return AccessType.offlineMultiple; + default: + throw ArgumentError("Invalid access type: $value"); + } + } +} + + + + + + +enum DeviseStatus { + online, + offline, +} + +extension OnlineTypeExtension on DeviseStatus { + String get value { + switch (this) { + case DeviseStatus.online: + return "Online"; + case DeviseStatus.offline: + return "Offline"; + + } + } + + static DeviseStatus fromString(bool value) { + switch (value) { + case false: + return DeviseStatus.offline; + case true: + return DeviseStatus.online; + default: + throw ArgumentError("Invalid access type: $value"); + } + } +} + + +enum AccessStatus { + expired , + effective , + toBeEffective, +} + +extension AccessStatusExtension on AccessStatus { + String get value { + switch (this) { + case AccessStatus.expired: + return "Expired"; + case AccessStatus.effective: + return "Effective" ; + case AccessStatus.toBeEffective: + return "To be effective"; + + } + } + + static AccessStatus fromString(String value) { + switch (value) { + case "EXPIRED" : + return AccessStatus.expired; + case "EFFECTIVE" : + return AccessStatus.effective; + case "TO_BE_EFFECTIVE": + return AccessStatus.toBeEffective; + default: + throw ArgumentError("Invalid access type: $value"); + } + } +} + + + + + diff --git a/lib/utils/constants/string_const.dart b/lib/utils/constants/string_const.dart deleted file mode 100644 index 3f2ff2d6..00000000 --- a/lib/utils/constants/string_const.dart +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/lib/utils/style.dart b/lib/utils/style.dart index 7260e78d..24747880 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -6,21 +6,41 @@ InputDecoration? textBoxDecoration({bool suffixIcon = false}) => InputDecoration suffixIcon:suffixIcon? const Icon(Icons.search):null, hintText: 'Search', filled: true, // Enable background filling - fillColor: Colors.grey.shade200, // Set the background color + fillColor: const Color(0xffF5F6F7), // Set the background color border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), // Add border radius + borderRadius: BorderRadius.circular(8), // Add border radius borderSide: BorderSide.none, // Remove the underline ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), // Add border radius + borderRadius: BorderRadius.circular(8), // Add border radius borderSide: BorderSide.none, // Remove the underline ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), // Add border radius + borderRadius: BorderRadius.circular(8), // Add border radius borderSide: BorderSide.none, // Remove the underline ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red, width: 2), + borderRadius: BorderRadius.circular(8), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red, width: 2), + borderRadius: BorderRadius.circular(8), + ), ); +BoxDecoration containerDecoration = BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 5, + blurRadius: 8, + offset: const Offset(0, + 3), // changes position of shadow + ), + ], + color: ColorsManager.boxColor, + borderRadius: const BorderRadius.all(Radius.circular(10))); + -Decoration containerDecoration = const BoxDecoration(color: Colors.white,borderRadius: BorderRadius.all(Radius.circular(20))); \ No newline at end of file diff --git a/lib/web_layout/web_app_bar.dart b/lib/web_layout/web_app_bar.dart index 4163e168..7b1ee1a7 100644 --- a/lib/web_layout/web_app_bar.dart +++ b/lib/web_layout/web_app_bar.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class WebAppBar extends StatelessWidget { @@ -8,8 +11,9 @@ class WebAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: 120, + return BlocBuilder(builder: (context, state) { + return Container( + height: 100, decoration: const BoxDecoration(color: ColorsManager.secondaryColor), padding: const EdgeInsets.all(10), child: Expanded( @@ -48,8 +52,9 @@ class WebAppBar extends StatelessWidget { const SizedBox( width: 10, ), + if(HomeBloc.user!=null) Text( - 'mohamamd alnemer ', + '${HomeBloc.user!.firstName.toString() ?? ''} ${HomeBloc.user!.lastName.toString() ?? ''} ', style: Theme.of(context).textTheme.bodyLarge, ), ], @@ -58,5 +63,6 @@ class WebAppBar extends StatelessWidget { ), ), ); + }); } } diff --git a/lib/web_layout/web_scaffold.dart b/lib/web_layout/web_scaffold.dart index e190dac1..31b8d958 100644 --- a/lib/web_layout/web_scaffold.dart +++ b/lib/web_layout/web_scaffold.dart @@ -3,14 +3,12 @@ import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/web_layout/web_app_bar.dart'; import 'menu_sidebar.dart'; - class WebScaffold extends StatelessWidget { final bool enableMenuSideba; final Widget? appBarTitle; final List? appBarBody; final Widget? scaffoldBody; const WebScaffold({super.key,this.appBarTitle,this.appBarBody,this.scaffoldBody,this.enableMenuSideba=true}); - @override Widget build(BuildContext context) { return Scaffold( @@ -29,7 +27,7 @@ class WebScaffold extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Opacity( - opacity: 0.6, + opacity: 0.7, child: WebAppBar( title: appBarTitle, body: appBarBody, diff --git a/pubspec.lock b/pubspec.lock index ba0d48bf..b8984d5a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + data_table_2: + dependency: "direct main" + description: + name: data_table_2 + sha256: f02ec9b24f44420816a87370ff4f4e533e15b274f6267e4c9a88a585ad1a0473 + url: "https://pub.dev" + source: hosted + version: "2.5.15" dio: dependency: "direct main" description: @@ -232,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7f55b18e..faaa7ddb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,8 @@ dependencies: get_it: ^7.6.7 flutter_secure_storage: ^9.2.2 shared_preferences: ^2.3.0 - + data_table_2: ^2.5.15 + intl: ^0.19.0 dev_dependencies: flutter_test: sdk: flutter @@ -69,6 +70,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/ # An image asset can refer to one or more resolution-specific "variants", see