From 2f233db3326c0d3846b0b87506d1a4e3fb9a7401 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:04:39 +0300 Subject: [PATCH] implemented space management side bar. --- .../views/space_management_page.dart | 81 ++++++-- .../widgets/space_management_body.dart | 15 ++ .../services/remote_communities_service.dart | 32 +++- .../domain/models/space_model.dart | 16 ++ .../communities_tree_selection_bloc.dart | 47 +++++ .../communities_tree_selection_event.dart | 30 +++ .../communities_tree_selection_state.dart | 29 +++ .../presentation/widgets/community_tile.dart | 37 ++++ .../widgets/create_community_dialog.dart | 181 ++++++++++++++++++ .../space_management_communities_tree.dart | 160 ++++++++++++++++ ...nagement_sidebar_add_community_button.dart | 34 ++++ ...e_management_sidebar_communities_list.dart | 72 +++++++ .../space_management_sidebar_header.dart | 36 ++++ .../presentation/widgets/space_tile.dart | 54 ++++++ .../all_spaces/widgets/space_tile_widget.dart | 3 +- lib/utils/app_routes.dart | 2 +- 16 files changed, 808 insertions(+), 21 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_body.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 03e17165..4c3c7452 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -1,5 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -8,20 +16,67 @@ class SpaceManagementPage extends StatelessWidget { @override Widget build(BuildContext context) { - return WebScaffold( - appBarTitle: Text( - 'Space Management', - style: ResponsiveTextTheme.of(context).deviceManagementTitle, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CommunitiesBloc( + communitiesService: _FakeCommunitiesService(), + )..add(const LoadCommunities(LoadCommunitiesParam())), + ), + BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + ], + child: WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const SpaceManagementBody(), ), - enableMenuSidebar: false, - centerBody: Text( - 'Community Structure', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - rightBody: const NavigateHomeGridView(), - scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} + +class _FakeCommunitiesService extends CommunitiesService { + @override + Future> getCommunity(LoadCommunitiesParam param) async { + return Future.delayed( + const Duration(seconds: 1), + () => [ + const CommunityModel( + uuid: '1', + name: 'Community 1', + spaces: [ + SpaceModel( + uuid: '3', + spaceName: 'Space 1', + icon: 'assets/icons/space.png', + children: [ + SpaceModel( + uuid: '4', + spaceName: 'Space 2', + icon: 'assets/icons/space.png', + children: [], + status: SpaceStatus.active, + ), + ], + status: SpaceStatus.active, + ), + ], + ), + const CommunityModel( + uuid: '2', + name: 'Community 1', + spaces: [], + ), + ], ); } } diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart new file mode 100644 index 00000000..3a9aa3c8 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; + +class SpaceManagementBody extends StatelessWidget { + const SpaceManagementBody({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + SpaceManagementCommunitiesTree(), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 36682bb4..83a212ca 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,9 +1,11 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteCommunitiesService implements CommunitiesService { const RemoteCommunitiesService(this._httpService); @@ -14,13 +16,27 @@ class RemoteCommunitiesService implements CommunitiesService { @override Future> getCommunity(LoadCommunitiesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is not set'); + try { - return _httpService.get( - path: '/api/communities/', - expectedResponseModel: (json) => (json as List) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), + final allCommunities = []; + await _httpService.get( + path: await _makeUrl(), + expectedResponseModel: (json) { + final response = json as Map; + final jsonData = response['data'] as List? ?? []; + return jsonData + .map( + (jsonItem) => CommunityModel.fromJson( + jsonItem as Map, + ), + ) + .toList(); + }, ); + + return allCommunities; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -31,4 +47,10 @@ class RemoteCommunitiesService implements CommunitiesService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is required'); + return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 0f8aadb2..519e8ee7 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,16 +1,31 @@ import 'package:equatable/equatable.dart'; +enum SpaceStatus { + active, + deleted, + parentDeleted; + + static SpaceStatus getValueFromString(String value) => switch (value) { + 'active' => active, + 'deleted' => deleted, + 'parentDeleted' => parentDeleted, + _ => active, + }; +} + class SpaceModel extends Equatable { final String uuid; final String spaceName; final String icon; final List children; + final SpaceStatus status; const SpaceModel({ required this.uuid, required this.spaceName, required this.icon, required this.children, + required this.status, }); factory SpaceModel.fromJson(Map json) { @@ -22,6 +37,7 @@ class SpaceModel extends Equatable { ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], + status: SpaceStatus.getValueFromString(json['status'] as String), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart new file mode 100644 index 00000000..bfc02f11 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -0,0 +1,47 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +part 'communities_tree_selection_event.dart'; +part 'communities_tree_selection_state.dart'; + +class CommunitiesTreeSelectionBloc + extends Bloc { + CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + on(_onSelectCommunity); + on(_onSelectSpace); + on(_onClearSelection); + } + + void _onSelectCommunity( + SelectCommunityEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: null, + ), + ); + } + + void _onSelectSpace( + SelectSpaceEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: null, + selectedSpace: event.space, + ), + ); + } + + void _onClearSelection( + ClearCommunitiesTreeSelectionEvent event, + Emitter emit, + ) { + emit(const CommunitiesTreeSelectionState()); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart new file mode 100644 index 00000000..95ffe173 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -0,0 +1,30 @@ +part of 'communities_tree_selection_bloc.dart'; + +sealed class CommunitiesTreeSelectionEvent extends Equatable { + const CommunitiesTreeSelectionEvent(); + + @override + List get props => []; +} + +final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { + final CommunityModel? community; + + const SelectCommunityEvent({required this.community}); + @override + List get props => [community]; +} + +final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { + final SpaceModel? space; + + const SelectSpaceEvent({required this.space}); + + @override + List get props => [space]; +} + +final class ClearCommunitiesTreeSelectionEvent + extends CommunitiesTreeSelectionEvent { + const ClearCommunitiesTreeSelectionEvent(); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart new file mode 100644 index 00000000..b14d330b --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -0,0 +1,29 @@ +part of 'communities_tree_selection_bloc.dart'; + +final class CommunitiesTreeSelectionState extends Equatable { + const CommunitiesTreeSelectionState({ + this.selectedCommunity, + this.selectedSpace, + }); + + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + + CommunitiesTreeSelectionState copyWith({ + CommunityModel? selectedCommunity, + SpaceModel? selectedSpace, + List? expandedCommunities, + List? expandedSpaces, + }) { + return CommunitiesTreeSelectionState( + selectedCommunity: selectedCommunity ?? this.selectedCommunity, + selectedSpace: selectedSpace ?? this.selectedSpace, + ); + } + + @override + List get props => [ + selectedCommunity, + selectedSpace, + ]; + } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart new file mode 100644 index 00000000..0baaae52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final void Function(String, bool isExpanded) onExpansionChanged; + final void Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + )); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart new file mode 100644 index 00000000..fd8a0a68 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialog extends StatefulWidget { + final void Function(String name) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + State createState() => _CreateCommunityDialogState(); +} + +class _CreateCommunityDialogState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CommunityDialogBloc([]), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + var isNameValid = true; + var isNameEmpty = false; + + if (state is CommunityNameValidationState) { + isNameValid = state.isNameValid; + isNameEmpty = state.isNameEmpty; + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + TextField( + controller: _nameController, + onChanged: (value) { + context + .read() + .add(ValidateCommunityNameEvent(value)); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: isNameValid && !isNameEmpty + ? ColorsManager.boxColor + : ColorsManager.red, + width: 1, + ), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + if (!isNameValid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exists.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + Expanded( + child: DefaultButton( + onPressed: () { + if (isNameValid && !isNameEmpty) { + widget.onCreateCommunity( + _nameController.text.trim(), + ); + Navigator.of(context).pop(); + } + }, + backgroundColor: isNameValid && !isNameEmpty + ? ColorsManager.secondaryColor + : ColorsManager.lightGrayColor, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart new file mode 100644 index 00000000..3248fa7d --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementCommunitiesTree extends StatelessWidget { + const SpaceManagementCommunitiesTree({super.key}); + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } + + static const _width = 300.0; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + width: _width, + decoration: subSectionContainerDecoration, + child: Column( + children: [ + SpaceManagementSidebarHeader( + onAddCommunity: () => _onAddCommunity(context), + ), + CustomSearchBar(onSearchChanged: (value) {}), + const SizedBox(height: 16), + switch (state.status) { + CommunitiesStatus.initial => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.loading => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.success => + _buildCommunitiesTree(context, state.communities), + CommunitiesStatus.failure => Center( + child: Text(state.errorMessage ?? 'Something went wrong'), + ), + }, + ], + ), + ); + }, + ); + } + + Widget _buildCommunitiesTree( + BuildContext context, + List communities, + ) { + return Expanded( + child: SpaceManagementSidebarCommunitiesList( + communities: communities, + itemBuilder: (context, index) { + return _buildCommunityTile(context, communities[index]); + }, + ), + ); + } + + Widget _buildCommunityTile(BuildContext context, CommunityModel community) { + final spaces = community.spaces + .where((space) => space.status == SpaceStatus.active) + .map((space) => _buildSpaceTile( + space: space, + community: community, + context: context, + )) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } + + Widget _buildSpaceTile({ + required SpaceModel space, + required CommunityModel community, + required BuildContext context, + }) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => _buildSpaceTile( + space: childSpace, + community: community, + context: context, + ), + ) + .toList(), + ), + ); + } + + void _onAddCommunity(BuildContext context) => context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + true + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + + void _clearSelection(BuildContext context) => + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (name) {}, + ), + ); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart new file mode 100644 index 00000000..ba281335 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart @@ -0,0 +1,34 @@ +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 SpaceManagementSidebarAddCommunityButton extends StatelessWidget { + const SpaceManagementSidebarAddCommunityButton({ + required this.onTap, + super.key, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + style: IconButton.styleFrom( + iconSize: 20, + backgroundColor: ColorsManager.circleImageBackground, + shape: const CircleBorder( + side: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 3, + ), + ), + ), + onPressed: onTap, + icon: SvgPicture.asset(Assets.addIcon), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart new file mode 100644 index 00000000..e7cb1ef6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceManagementSidebarCommunitiesList extends StatefulWidget { + const SpaceManagementSidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + + @override + State createState() => + _SpaceManagementSidebarCommunitiesListState(); +} + +class _SpaceManagementSidebarCommunitiesListState + extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd) { + // Call data from API. + return true; + } + + return false; + } + + @override + void dispose() { + _scrollController + ..removeListener(() {}) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: context.screenWidth * 0.5, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.left, + thumbVisibility: true, + controller: _scrollController, + child: NotificationListener( + onNotification: _onNotification, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsetsDirectional.only(start: 16), + itemCount: widget.communities.length, + controller: _scrollController, + itemBuilder: widget.itemBuilder, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart new file mode 100644 index 00000000..cf40f95c --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementSidebarHeader extends StatelessWidget { + const SpaceManagementSidebarHeader({ + required this.onAddCommunity, + super.key, + }); + + final void Function() onAddCommunity; + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Communities', + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + SpaceManagementSidebarAddCommunityButton( + onTap: onAddCommunity, + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart new file mode 100644 index 00000000..d05199f0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final void Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + State createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0), + child: CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart index d72f22ac..d81a3b04 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart @@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; class SpaceTile extends StatefulWidget { final String title; final bool isSelected; - final bool initiallyExpanded; final ValueChanged onExpansionChanged; final List? children; - final Function() onItemSelected; + final void Function() onItemSelected; const SpaceTile({ super.key, diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart';