diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg new file mode 100644 index 00000000..2b4be77b --- /dev/null +++ b/assets/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow_forward.svg b/assets/icons/arrow_forward.svg new file mode 100644 index 00000000..e5866360 --- /dev/null +++ b/assets/icons/arrow_forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/pages/roles_and_permission/users_page/bloc/users_bloc.dart b/lib/pages/roles_and_permission/users_page/bloc/users_bloc.dart index 5de74e4b..0d153804 100644 --- a/lib/pages/roles_and_permission/users_page/bloc/users_bloc.dart +++ b/lib/pages/roles_and_permission/users_page/bloc/users_bloc.dart @@ -3,12 +3,18 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/model/tree_node_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; class UsersBloc extends Bloc { UsersBloc() : super(UsersInitial()) { on(_getUsers); on(_changeUserStatus); on(isCompleteBasicsFun); + on(_onLoadCommunityAndSpaces); + on(searchTreeNode); } List users = []; @@ -86,9 +92,9 @@ class UsersBloc extends Bloc { final TextEditingController phoneController = TextEditingController(); final TextEditingController jobTitleController = TextEditingController(); - bool isCompleteBasics = false; - bool isCompleteRolePermissions = false; - bool isCompleteSpaces = false; + bool? isCompleteBasics; + bool? isCompleteRolePermissions; + bool? isCompleteSpaces; int numberBasics = 0; int numberSpaces = 0; @@ -102,35 +108,137 @@ class UsersBloc extends Bloc { phoneController.text.isNotEmpty && jobTitleController.text.isNotEmpty; emit(ChangeStatusSteps()); + return isCompleteBasics; } - // void checkStatus(CheckStepStatus event, Emitter emit) { - // try { - // // Check if basic fields are completed - // isCompleteBasics = firstNameController.text.isNotEmpty && - // lastNameController.text.isNotEmpty && - // emailController.text.isNotEmpty && - // phoneController.text.isNotEmpty && - // jobTitleController.text.isNotEmpty; - // // Emit the updated state - // if (isCompleteBasics && isCompleteRolePermissions && isCompleteSpaces) { - // } else { - // // emit(IncompleteState( - // // isCompleteBasics, isCompleteRolePermissions, isCompleteSpaces)); - // } - // } catch (e) { - // emit(ErrorState(e.toString())); - // } - // } + isCompleteSpacesFun(CheckStepStatus event, Emitter emit) { + emit(UsersLoadingState()); + isCompleteSpaces = false; + emit(ChangeStatusSteps()); + print('isCompleteBasics==$isCompleteSpaces'); + return isCompleteSpaces; + } -// Example placeholder methods for role permissions and spaces bool checkRolePermissions() { - // Add logic to check if role permissions are completed - return true; // Replace with actual logic + return true; } bool checkSpaces() { - // Add logic to check if spaces are completed - return true; // Replace with actual logic + return true; + } + + Future> _fetchSpacesForCommunity( + String communityUuid) async { + return await CommunitySpaceManagementApi().getSpaceHierarchy(communityUuid); + } + + List updatedCommunities = []; + List spacesNodes = []; + _onLoadCommunityAndSpaces( + LoadCommunityAndSpacesEvent event, + Emitter emit, + ) async { + emit(UsersLoadingState()); // Emit loading state + try { + // Fetch the list of communities + List communities = + await CommunitySpaceManagementApi().fetchCommunities(); + + // Fetch spaces and create TreeNodes for each community + updatedCommunities = await Future.wait( + communities.map((community) async { + // Fetch spaces for the current community + List spaces = + await _fetchSpacesForCommunity(community.uuid); + + // Recursively build the tree structure + spacesNodes = _buildTreeNodes(spaces); + + // Return a TreeNode for the community, with spaces as its children + return TreeNode( + uuid: community.uuid, + title: community.name, + children: spacesNodes, + isChecked: false, // Initial state; can be updated later + isHighlighted: false, + isExpanded: true, // Default to expanded for better UX + ); + }).toList(), + ); + + // Emit the final state with the structured tree + emit(ChangeStatusSteps()); + + return updatedCommunities; // Return the structured data if needed + } catch (e) { + // Emit error state in case of failure + emit(ErrorState('Error loading communities and spaces: $e')); + } + } + +// Helper function to recursively build tree nodes + List _buildTreeNodes(List spaces) { + return spaces.map((space) { + // If the space has children, recursively build nodes for them + List childNodes = + space.children != null ? _buildTreeNodes(space.children!) : []; + + // Create a TreeNode for the current space + return TreeNode( + uuid: space.uuid!, + title: space.name, + isChecked: false, + isHighlighted: false, + isExpanded: childNodes.isNotEmpty, // Expand if there are children + children: childNodes, + ); + }).toList(); + } + + void searchTreeNode(SearchAnode event, Emitter emit) { + emit(UsersLoadingState()); // Emit loading state + + // Clear all highlights if the search term is empty + if (event.searchTerm!.isEmpty) { + _clearHighlights(updatedCommunities); + } else { + // Perform the search and update the highlights + _searchAndHighlightNodes(updatedCommunities, event.searchTerm!); + } + + // Emit the updated state after processing all nodes + emit(ChangeStatusSteps()); + } + +// Helper function to clear all highlights in the tree + void _clearHighlights(List nodes) { + for (var node in nodes) { + node.isHighlighted = false; + if (node.children.isNotEmpty) { + _clearHighlights(node.children); + } + } + } + +// Helper function to search and highlight nodes recursively + bool _searchAndHighlightNodes(List nodes, String searchTerm) { + bool anyMatch = false; + + for (var node in nodes) { + // Check if this node matches the search term + bool isMatch = + node.title.toLowerCase().contains(searchTerm.toLowerCase()); + + // Recursively check children for matches + bool childMatch = _searchAndHighlightNodes(node.children, searchTerm); + + // Highlight this node if it matches or any of its children match + node.isHighlighted = isMatch || childMatch; + + // Update if any matches were found in this branch + anyMatch = anyMatch || node.isHighlighted; + } + + return anyMatch; } } diff --git a/lib/pages/roles_and_permission/users_page/bloc/users_event.dart b/lib/pages/roles_and_permission/users_page/bloc/users_event.dart index 3b679a7e..77890673 100644 --- a/lib/pages/roles_and_permission/users_page/bloc/users_event.dart +++ b/lib/pages/roles_and_permission/users_page/bloc/users_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/model/tree_node_model.dart'; sealed class UsersEvent extends Equatable { const UsersEvent(); @@ -10,6 +11,12 @@ class GetUsers extends UsersEvent { List get props => []; } +class LoadCommunityAndSpacesEvent extends UsersEvent { + const LoadCommunityAndSpacesEvent(); + @override + List get props => []; +} + class GetBatchStatus extends UsersEvent { final List uuids; const GetBatchStatus(this.uuids); @@ -17,6 +24,7 @@ class GetBatchStatus extends UsersEvent { List get props => [uuids]; } +//LoadCommunityAndSpacesEvent class ChangeUserStatus extends UsersEvent { final String userId; final String newStatus; @@ -33,3 +41,11 @@ class CheckStepStatus extends UsersEvent { @override List get props => [steps]; } + +class SearchAnode extends UsersEvent { + List? nodes; + String? searchTerm; + SearchAnode({this.nodes, this.searchTerm}); + @override + List get props => [nodes, searchTerm]; +} diff --git a/lib/pages/roles_and_permission/users_page/model/tree_node_model.dart b/lib/pages/roles_and_permission/users_page/model/tree_node_model.dart new file mode 100644 index 00000000..a5e622dc --- /dev/null +++ b/lib/pages/roles_and_permission/users_page/model/tree_node_model.dart @@ -0,0 +1,17 @@ +class TreeNode { + String uuid; + String title; + bool isChecked; + bool isHighlighted; + bool isExpanded; + List children; + + TreeNode({ + required this.uuid, + required this.title, + this.isChecked = false, + this.isHighlighted = false, + this.isExpanded = false, + this.children = const [], + }); +} \ No newline at end of file diff --git a/lib/pages/roles_and_permission/users_page/view/add_user_dialog.dart b/lib/pages/roles_and_permission/users_page/view/add_user_dialog.dart index b2ee8cf5..21e92727 100644 --- a/lib/pages/roles_and_permission/users_page/view/add_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/view/add_user_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_status.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/view/basics_view.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/view/roles_and_permission.dart'; @@ -22,7 +23,7 @@ class _AddNewUserDialogState extends State { @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => UsersBloc(), + create: (BuildContext context) => UsersBloc()..add(LoadCommunityAndSpacesEvent()), child: BlocConsumer( listener: (context, state) {}, builder: (context, state) { @@ -58,9 +59,9 @@ class _AddNewUserDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStepIndicator(1, "Basics", _blocRole), - _buildStepIndicator(2, "Spaces", _blocRole), - _buildStepIndicator( + _buildStep1Indicator(1, "Basics", _blocRole), + _buildStep2Indicator(2, "Spaces", _blocRole), + _buildStep3Indicator( 3, "Role & Permissions", _blocRole), ], ), @@ -120,14 +121,13 @@ class _AddNewUserDialogState extends State { ), ], ), - ), - ], - ), - )); - })); + ), + ], + ), + )); + })); } - // Method to get the form content based on the current step Widget _getFormContent() { switch (currentStep) { case 1: @@ -141,22 +141,70 @@ class _AddNewUserDialogState extends State { } } - // Helper method to build step indicators - Widget _buildStepIndicator(int step, String label, UsersBloc bloc) { + Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + currentStep = step; + bloc.add(const CheckStepStatus()); + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteSpaces == false + ? Assets.wrongProcessIcon + : Assets.uncomplete_ProcessIcon, + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } + + Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { return GestureDetector( onTap: () { - // if (bloc.formKey.currentState?.validate() ?? false) { setState(() { currentStep = step; - if (currentStep == 1) { - bloc.numberBasics = 1; - } else if (currentStep == 2) { - bloc.numberSpaces = 2; - } else if (currentStep == 3) { - bloc.numberRole = 3; - } }); - // } }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -170,12 +218,67 @@ class _AddNewUserDialogState extends State { currentStep == step ? Assets.currentProcessIcon : bloc.isCompleteBasics == false - && (bloc.numberBasics != 0 || - bloc.numberRole != 0 || - bloc.numberSpaces != 0) - ? Assets.wrongProcessIcon + ? Assets.wrongProcessIcon + : bloc.isCompleteBasics == true + ? Assets.completeProcessIcon : Assets.uncomplete_ProcessIcon, - + width: 25, + height: 25, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontSize: 16, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + if (step != 3) + Padding( + padding: const EdgeInsets.all(5.0), + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Container( + height: 60, + width: 1, + color: Colors.grey, + ), + ), + ) + ], + ), + ); + } + + Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + return GestureDetector( + onTap: () { + setState(() { + currentStep = step; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + SvgPicture.asset( + currentStep == step + ? Assets.currentProcessIcon + : bloc.isCompleteRolePermissions == false + ? Assets.wrongProcessIcon + : Assets.uncomplete_ProcessIcon, width: 25, height: 25, ), diff --git a/lib/pages/roles_and_permission/users_page/view/roles_and_permission.dart b/lib/pages/roles_and_permission/users_page/view/roles_and_permission.dart index c9b58581..812a3170 100644 --- a/lib/pages/roles_and_permission/users_page/view/roles_and_permission.dart +++ b/lib/pages/roles_and_permission/users_page/view/roles_and_permission.dart @@ -199,6 +199,8 @@ class DeviceManagement extends StatefulWidget { } class _DeviceManagementState extends State { + + final List options = [ MainRoleOption( id: '1', diff --git a/lib/pages/roles_and_permission/users_page/view/spaces_access_view.dart b/lib/pages/roles_and_permission/users_page/view/spaces_access_view.dart index 9dc35a89..5dff9b0a 100644 --- a/lib/pages/roles_and_permission/users_page/view/spaces_access_view.dart +++ b/lib/pages/roles_and_permission/users_page/view/spaces_access_view.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/bloc/users_status.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/model/tree_node_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -68,7 +70,12 @@ class SpacesAccessView extends StatelessWidget { child: TextFormField( style: const TextStyle(color: Colors.black), - controller: _blocRole.firstNameController, + // controller: _blocRole.firstNameController, + onChanged: (value) { + _blocRole.add(SearchAnode( + nodes: _blocRole.updatedCommunities, + searchTerm: value)); + }, decoration: textBoxDecoration(radios: 20)! .copyWith( fillColor: Colors.white, @@ -102,7 +109,9 @@ class SpacesAccessView extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Container( color: ColorsManager.whiteColors, - child: TreeView()))) + child: TreeView( + bloc: _blocRole, + )))) ], ), ), @@ -115,90 +124,15 @@ class SpacesAccessView extends StatelessWidget { } } -class TreeNode { - String title; - bool isChecked; - bool isHighlighted; - bool isExpanded; // New property to manage expansion - List children; - - TreeNode({ - required this.title, - this.isChecked = false, - this.isHighlighted = false, - this.isExpanded = false, // Default to collapsed - this.children = const [], - }); -} - +// ignore: must_be_immutable class TreeView extends StatefulWidget { + UsersBloc? bloc; + TreeView({super.key, this.bloc}); @override _TreeViewState createState() => _TreeViewState(); } class _TreeViewState extends State { - List treeData = [ - TreeNode( - title: 'Downtown Dubai', - ), - TreeNode(title: 'Dubai Creek Harbour', isHighlighted: true), - TreeNode( - title: 'Dubai Hills Estate', - isHighlighted: true, - children: [ - TreeNode(title: 'North Side'), - TreeNode( - title: 'South Side', - isHighlighted: true, - children: [ - TreeNode(title: 'Hills Business Park'), - TreeNode(title: 'Park Point'), - TreeNode(title: 'Acacia'), - TreeNode( - title: 'Executive Residence', - children: [ - TreeNode(title: 'Residence I'), - TreeNode( - title: 'Residence II', - children: [ - TreeNode(title: 'Ground Floor', isHighlighted: true), - TreeNode(title: '1st Floor', isHighlighted: true), - TreeNode(title: 'Pool', isHighlighted: true), - TreeNode(title: 'Gym', isHighlighted: true), - ], - ), - ], - ), - ], - ), - TreeNode( - title: 'South Side', - isHighlighted: true, - children: [ - TreeNode(title: 'Hills Business Park'), - TreeNode(title: 'Park Point'), - TreeNode(title: 'Acacia'), - TreeNode( - title: 'Executive Residence', - children: [ - TreeNode(title: 'Residence I'), - TreeNode( - title: 'Residence II', - children: [ - TreeNode(title: 'Ground Floor', isHighlighted: true), - TreeNode(title: '1st Floor', isHighlighted: true), - TreeNode(title: 'Pool', isHighlighted: true), - TreeNode(title: 'Gym', isHighlighted: true), - ], - ), - ], - ), - ], - ), - ], - ), - ]; - Widget _buildTree(List nodes, {int level = 0}) { return Column( children: nodes.map((node) => _buildNode(node, level: level)).toList(), @@ -207,31 +141,14 @@ class _TreeViewState extends State { Widget _buildNode(TreeNode node, {int level = 0}) { return Container( - color: node.isHighlighted ? Colors.blue.shade100 : Colors.transparent, - child: Padding( - padding: EdgeInsets.only(left: level * 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + color: node.isHighlighted ? Colors.blue.shade50 : Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( children: [ - GestureDetector( - onTap: () { - setState(() { - node.isExpanded = !node.isExpanded; // Toggle expansion - }); - }, - child: Icon( - node.children.isNotEmpty - ? (node.isExpanded - ? Icons.arrow_drop_down - : Icons.arrow_right) - : Icons.arrow_right, - color: node.children.isNotEmpty - ? Colors.black - : Colors.transparent, - ), - ), GestureDetector( onTap: () { setState(() { @@ -246,24 +163,49 @@ class _TreeViewState extends State { height: 20, ), ), + const SizedBox(width: 15), Expanded( - child: Text( - node.title, - style: TextStyle( - fontSize: 16, - color: Colors.black87, + child: Padding( + padding: EdgeInsets.only(left: level * 10.0), + child: Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + node.isExpanded = !node.isExpanded; + }); + }, + child: SizedBox( + child: SvgPicture.asset( + node.children.isNotEmpty + ? (node.isExpanded + ? Assets.arrowDown + : Assets.arrowForward) + : Assets.arrowForward, + fit: BoxFit.none, + ), + ), + ), + const SizedBox(width: 20), + Text( + node.title, + style: TextStyle( + fontSize: 16, + color: node.isHighlighted + ? ColorsManager.blackColor + : ColorsManager.textGray, + ), + ), + ], ), ), ), ], ), - if (node.isExpanded && node.children.isNotEmpty) - Padding( - padding: const EdgeInsets.only(left: 24.0), - child: _buildTree(node.children, level: level + 1), - ), - ], - ), + ), + if (node.isExpanded && node.children.isNotEmpty) + _buildTree(node.children, level: level + 1), + ], ), ); } @@ -308,7 +250,7 @@ class _TreeViewState extends State { // Update the checkbox state for parent nodes void _updateParentCheckStatus(TreeNode node) { - TreeNode? parent = _findParent(treeData, node); + TreeNode? parent = _findParent(widget.bloc!.updatedCommunities, node); if (parent != null) { setState(() { parent.isChecked = _areAllChildrenChecked(parent); @@ -332,7 +274,7 @@ class _TreeViewState extends State { @override Widget build(BuildContext context) { return SingleChildScrollView( - child: _buildTree(treeData), + child: _buildTree(widget.bloc!.updatedCommunities), ); } } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 5d2464e6..58dc0d9d 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -252,6 +252,7 @@ class CommunitySpaceManagementApi { path: ApiEndpoints.getSpaceHierarchy .replaceAll('{communityId}', communityId), expectedResponseModel: (json) { + print('=-=-=-=$json'); final spaceModels = (json['data'] as List) .map((spaceJson) => SpaceModel.fromJson(spaceJson)) .toList(); diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 4e35e84f..acb314d9 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -391,4 +391,8 @@ class Assets { 'assets/icons/uncompleate_process_icon.svg'; static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg'; + static const String arrowForward = + 'assets/icons/arrow_forward.svg'; + static const String arrowDown = + 'assets/icons/arrow_down.svg'; }