From f7e4d6ff0717facb49447c06cedadea90647f156 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 09:33:45 +0300 Subject: [PATCH 01/29] added default dialog background color to be white. --- lib/utils/theme/theme.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/utils/theme/theme.dart b/lib/utils/theme/theme.dart index 5ac61afa..5c036762 100644 --- a/lib/utils/theme/theme.dart +++ b/lib/utils/theme/theme.dart @@ -52,4 +52,7 @@ final myTheme = ThemeData( borderRadius: BorderRadius.circular(4), ), ), + dialogTheme: const DialogThemeData( + backgroundColor: ColorsManager.whiteColors, + ), ); From 62a6f9c993e2b64d45d017c041ef8b3c6ff1062a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:03 +0300 Subject: [PATCH 02/29] Add ButtonContentWidget for customizable button UI in space details. --- .../widgets/button_content_widget.dart | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart new file mode 100644 index 00000000..4c95634e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ButtonContentWidget extends StatelessWidget { + final String label; + final String? svgAssets; + final bool disabled; + + const ButtonContentWidget({ + required this.label, + this.svgAssets, + this.disabled = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Opacity( + opacity: disabled ? 0.5 : 1.0, + child: Container( + width: screenWidth * 0.25, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + border: Border.all( + color: ColorsManager.neutralGray, + width: 3.0, + ), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Row( + children: [ + if (svgAssets != null) + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + svgAssets!, + width: screenWidth * 0.015, + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle( + color: ColorsManager.blackColor, + fontSize: 16, + ), + ), + ), + ], + ), + ), + ); + } +} From 87c2e3261dce3f3dcdd3bea074c8ba0163a2a486 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:13 +0300 Subject: [PATCH 03/29] Add SpaceDetailsActionButtons widget for improved action handling in space details. --- .../widgets/space_details_action_buttons.dart | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart new file mode 100644 index 00000000..b26a6590 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.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/utils/color_manager.dart'; + +class SpaceDetailsActionButtons extends StatelessWidget { + const SpaceDetailsActionButtons({ + super.key, + required this.onSave, + required this.onCancel, + }); + + final VoidCallback onCancel; + final VoidCallback onSave; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 10, + children: [ + Expanded(child: _buildCancelButton(context)), + Expanded(child: _buildSaveButton()), + ], + ); + } + + Widget _buildCancelButton(BuildContext context) { + return CancelButton( + onPressed: onCancel, + label: 'Cancel', + ); + } + + Widget _buildSaveButton() { + return Expanded( + child: DefaultButton( + onPressed: onSave, + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } +} From 50ff17a0c17b346d090736573af46d1347707e1c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:26 +0300 Subject: [PATCH 04/29] Add SpaceIconSelectionDialog widget for selecting space icons in a dialog. --- .../widgets/space_icon_selection_dialog.dart | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart new file mode 100644 index 00000000..428048b3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart @@ -0,0 +1,62 @@ +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'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceIconSelectionDialog extends StatelessWidget { + const SpaceIconSelectionDialog({super.key}); + + static const List _icons = [ + Assets.location, + Assets.villa, + Assets.gym, + Assets.sauna, + Assets.bbq, + Assets.building, + Assets.desk, + Assets.door, + Assets.parking, + Assets.pool, + Assets.stair, + Assets.steamRoom, + Assets.street, + Assets.unit, + ]; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: SelectableText( + 'Space Icon', + style: context.textTheme.headlineMedium, + ), + backgroundColor: ColorsManager.whiteColors, + content: Container( + width: context.screenWidth * 0.45, + height: context.screenHeight * 0.275, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(12), + ), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 8, + mainAxisSpacing: 16, + ), + itemCount: _icons.length, + itemBuilder: (context, index) => IconButton( + onPressed: Navigator.of(context).pop, + icon: SvgPicture.asset( + _icons[index], + width: context.screenWidth * 0.03, + height: context.screenHeight * 0.08, + ), + ), + ), + ), + ); + } +} From bdeec7d325ea07ddbc621c497ac235fb0c082cd2 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 11:27:36 +0300 Subject: [PATCH 05/29] Add SpaceIconPicker widget for selecting and displaying space icons with a dialog option. --- .../widgets/space_icon_picker.dart | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart new file mode 100644 index 00000000..00552b88 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.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'; + +class SpaceIconPicker extends StatelessWidget { + const SpaceIconPicker({ + required this.iconPath, + super.key, + }); + + final String iconPath; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Stack( + alignment: Alignment.center, + children: [ + Container( + width: context.screenWidth * 0.1, + height: context.screenWidth * 0.1, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + ), + SvgPicture.asset( + iconPath, + width: context.screenWidth * 0.04, + height: context.screenWidth * 0.04, + ), + Positioned( + top: 20, + right: 20, + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (context) => const SpaceIconSelectionDialog(), + ), + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ), + ), + ], + ), + ], + ); + } +} From fdd0526c78c17d955edcab3a1e2d4393c67c3219 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 14:17:27 +0300 Subject: [PATCH 06/29] added copyWith to `SpaceDetailsModel` and its property models. --- .../domain/models/space_details_model.dart | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 891e7eb2..8e8bbb90 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -41,6 +41,22 @@ class SpaceDetailsModel extends Equatable { }; } + SpaceDetailsModel copyWith({ + String? uuid, + String? spaceName, + String? icon, + List? productAllocations, + List? subspaces, + }) { + return SpaceDetailsModel( + uuid: uuid ?? this.uuid, + spaceName: spaceName ?? this.spaceName, + icon: icon ?? this.icon, + productAllocations: productAllocations ?? this.productAllocations, + subspaces: subspaces ?? this.subspaces, + ); + } + @override List get props => [uuid, spaceName, icon, productAllocations, subspaces]; } @@ -70,6 +86,18 @@ class ProductAllocation extends Equatable { }; } + ProductAllocation copyWith({ + Product? product, + Tag? tag, + String? location, + }) { + return ProductAllocation( + product: product ?? this.product, + tag: tag ?? this.tag, + location: location ?? this.location, + ); + } + @override List get props => [product, tag]; } @@ -103,6 +131,18 @@ class Subspace extends Equatable { }; } + Subspace copyWith({ + String? uuid, + String? name, + List? productAllocations, + }) { + return Subspace( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + productAllocations: productAllocations ?? this.productAllocations, + ); + } + @override List get props => [uuid, name, productAllocations]; } From 3601b02bc3bb7404310f43698d4f4bd3f46f68d5 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:02:55 +0300 Subject: [PATCH 07/29] Add SpaceDetailsModelBloc and events for managing space details state --- .../views/space_management_page.dart | 2 + .../space_details_model_bloc.dart | 43 ++++++++++++++++++ .../space_details_model_event.dart | 44 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart create mode 100644 lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.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 957be65a..c44236df 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 @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s 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/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/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -18,6 +19,7 @@ class SpaceManagementPage extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ + BlocProvider(create: (context) => SpaceDetailsModelBloc()), BlocProvider( create: (context) => CommunitiesBloc( communitiesService: DebouncedCommunitiesService( diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart new file mode 100644 index 00000000..57e794e8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -0,0 +1,43 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; + +part 'space_details_model_event.dart'; + +class SpaceDetailsModelBloc extends Bloc { + SpaceDetailsModelBloc() : super(SpaceDetailsModel.empty()) { + on(_onUpdateSpaceDetailsIcon); + on(_onUpdateSpaceDetailsName); + on(_onUpdateSpaceDetailsSubspaces); + on( + _onUpdateSpaceDetailsProductAllocations); + } + + void _onUpdateSpaceDetailsIcon( + UpdateSpaceDetailsIcon event, + Emitter emit, + ) { + emit(state.copyWith(icon: event.icon)); + } + + void _onUpdateSpaceDetailsName( + UpdateSpaceDetailsName event, + Emitter emit, + ) { + emit(state.copyWith(spaceName: event.name)); + } + + void _onUpdateSpaceDetailsSubspaces( + UpdateSpaceDetailsSubspaces event, + Emitter emit, + ) { + emit(state.copyWith(subspaces: event.subspaces)); + } + + void _onUpdateSpaceDetailsProductAllocations( + UpdateSpaceDetailsProductAllocations event, + Emitter emit, + ) { + emit(state.copyWith(productAllocations: event.productAllocations)); + } +} diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart new file mode 100644 index 00000000..d3e04bb9 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart @@ -0,0 +1,44 @@ +part of 'space_details_model_bloc.dart'; + +sealed class SpaceDetailsModelEvent extends Equatable { + const SpaceDetailsModelEvent(); + + @override + List get props => []; +} + +final class UpdateSpaceDetailsIcon extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsIcon(this.icon); + + final String icon; + + @override + List get props => [icon]; +} + +final class UpdateSpaceDetailsName extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsName(this.name); + + final String name; + + @override + List get props => [name]; +} + +final class UpdateSpaceDetailsSubspaces extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsSubspaces(this.subspaces); + + final List subspaces; + + @override + List get props => [subspaces]; +} + +final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsProductAllocations(this.productAllocations); + + final List productAllocations; + + @override + List get props => [productAllocations]; +} From fa6ee9a0afbf112dc571fc87939d910fdc3f308c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:00 +0300 Subject: [PATCH 08/29] Add factory method `empty` to `SpaceDetailsModel` for creating default instances --- .../space_details/domain/models/space_details_model.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 8e8bbb90..8b9ff666 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class SpaceDetailsModel extends Equatable { final String uuid; @@ -17,6 +18,13 @@ class SpaceDetailsModel extends Equatable { required this.subspaces, }); + factory SpaceDetailsModel.empty() => const SpaceDetailsModel( + uuid: '', + spaceName: '', + icon: Assets.villa, + productAllocations: [], + subspaces: [], + ); factory SpaceDetailsModel.fromJson(Map json) { return SpaceDetailsModel( uuid: json['uuid'] as String, From 68b6c9b18c4db1e88ed3bbbe93dc81b5d9cd3775 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:07 +0300 Subject: [PATCH 09/29] Refactor SpaceDetailsBloc to move SpaceDetailsService declaration for improved clarity --- .../space_details/presentation/bloc/space_details_bloc.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart index 59c1a06d..6eb96632 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart @@ -9,12 +9,12 @@ part 'space_details_event.dart'; part 'space_details_state.dart'; class SpaceDetailsBloc extends Bloc { - final SpaceDetailsService _spaceDetailsService; - SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) { on(_onLoadSpaceDetails); } + final SpaceDetailsService _spaceDetailsService; + Future _onLoadSpaceDetails( LoadSpaceDetails event, Emitter emit, From 63353af38bd5885f8a98bb2596e243a95770b964 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:23 +0300 Subject: [PATCH 10/29] Add SpaceDetails dialog and related widgets for creating and editing spaces, including SpaceDetailsDevicesBox and SpaceSubSpacesBox for managing devices and subspaces. --- .../helpers/space_details_dialog_helper.dart | 28 +++- .../widgets/space_details_action_buttons.dart | 14 +- .../widgets/space_details_devices_box.dart | 90 +++++++++++ .../widgets/space_details_dialog.dart | 72 ++++++++- .../widgets/space_name_text_field.dart | 84 ++++++++++ .../widgets/space_sub_spaces_box.dart | 67 ++++++++ .../widgets/space_sub_spaces_dialog.dart | 143 ++++++++++++++++++ .../presentation/widgets/subspace_chip.dart | 55 +++++++ .../widgets/subspace_name_display_widget.dart | 73 +++++++++ 9 files changed, 614 insertions(+), 12 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index e871f4d0..5d6ffee7 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,11 +1,37 @@ import 'package:flutter/material.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/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; abstract final class SpaceDetailsDialogHelper { static void showCreate(BuildContext context) { showDialog( context: context, - builder: (context) => const SpaceDetailsDialog(), + builder: (_) => SpaceDetailsDialog( + title: const Text('Create Space'), + space: SpaceDetailsModel.empty(), + onSave: (space) {}, + ), + ); + } + + static void showEdit( + BuildContext context, { + required SpaceModel spaceModel, + }) { + showDialog( + context: context, + builder: (_) => SpaceDetailsDialog( + title: const Text('Edit Space'), + space: SpaceDetailsModel( + uuid: spaceModel.uuid, + spaceName: spaceModel.spaceName, + icon: spaceModel.icon, + productAllocations: const [], + subspaces: const [], + ), + onSave: (space) {}, + ), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index b26a6590..502ae4bb 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -33,14 +33,12 @@ class SpaceDetailsActionButtons extends StatelessWidget { } Widget _buildSaveButton() { - return Expanded( - child: DefaultButton( - onPressed: onSave, - borderRadius: 10, - backgroundColor: ColorsManager.secondaryColor, - foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), - ), + return DefaultButton( + onPressed: onSave, + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart new file mode 100644 index 00000000..4873dc09 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.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'; + +class SpaceDetailsDevicesBox extends StatelessWidget { + const SpaceDetailsDevicesBox({super.key, required this.space}); + + final SpaceDetailsModel space; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (space.productAllocations.isNotEmpty || + space.subspaces + .any((subspace) => subspace.productAllocations.isNotEmpty)) + SizedBox( + width: context.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + // ...TagHelper.groupTags([ + // ...?tags, + // ...?subspaces?.expand((subspace) => subspace.tags ?? []) + // ]).entries.map( + // (entry) => Chip( + // avatar: SizedBox( + // width: 24, + // height: 24, + // child: SvgPicture.asset( + // entry.key.icon ?? 'assets/icons/gateway.svg', + // fit: BoxFit.contain, + // ), + // ), + // label: Text( + // 'x${entry.value}', // Show count + // style: Theme.of(context) + // .textTheme + // .bodySmall + // ?.copyWith(color: ColorsManager.spaceColor), + // ), + // backgroundColor: ColorsManager.whiteColors, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // side: const BorderSide( + // color: ColorsManager.spaceColor, + // ), + // ), + // ), + // ), + + EditChip( + onTap: () {}, + ), + ], + ), + ), + ) + else + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Add Devices', + // disabled: isTagsAndSubspaceModelDisabled, + ), + ) + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 7213c99e..43b52ef1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,12 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' + show SpaceDetailsModel; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDialog extends StatelessWidget { - const SpaceDetailsDialog({super.key}); + const SpaceDetailsDialog({ + required this.title, + required this.space, + required this.onSave, + super.key, + }); + + final Widget title; + final SpaceDetailsModel space; + final void Function(SpaceDetailsModel space) onSave; @override Widget build(BuildContext context) { - return const Dialog( - child: Text('Create Space'), + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: SpaceIconPicker( + iconPath: space.icon, + ), + ), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text(context.watch().state.toString()), + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], + ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart new file mode 100644 index 00000000..d5bd1016 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceNameTextField extends StatefulWidget { + const SpaceNameTextField({ + required this.initialValue, + required this.isNameFieldExist, + super.key, + }); + + final String? initialValue; + final bool Function(String value) isNameFieldExist; + + @override + State createState() => _SpaceNameTextFieldState(); +} + +class _SpaceNameTextFieldState extends State { + late final TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(text: widget.initialValue); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + + String? _validateName(String? value) { + if (value == null || value.isEmpty) { + return '*Space name should not be empty.'; + } + if (widget.isNameFieldExist(value)) { + return '*Name already exists'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + controller: _controller, + onChanged: (value) => context.read().add( + UpdateSpaceDetailsName(value), + ), + validator: _validateName, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.lightGrayColor, + ), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + ), + ), + errorStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart new file mode 100644 index 00000000..071d6fec --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.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'; + +class SpaceSubSpacesBox extends StatelessWidget { + const SpaceSubSpacesBox({super.key, required this.subspaces}); + + final List subspaces; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (subspaces.isEmpty) + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () => showDialog( + context: context, + builder: (_) => SpaceSubSpacesDialog(subspaces: subspaces), + ), + child: const ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Create Sub Spaces', + // disabled: widget.isTagsAndSubspaceModelDisabled, + disabled: false, + ), + ) + else + SizedBox( + width: context.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map( + (e) => SubspaceNameDisplayWidget(subSpace: e), + ), + EditChip( + onTap: () {}, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart new file mode 100644 index 00000000..1c435d8b --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceSubSpacesDialog extends StatelessWidget { + const SpaceSubSpacesDialog({ + required this.subspaces, + super.key, + }); + + final List subspaces; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create Sub Spaces'), + content: TextFieldSubSpaceDialogWidget( + subSpaces: subspaces, + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: Navigator.of(context).pop, + ) + ], + ); + } +} + +class TextFieldSubSpaceDialogWidget extends StatefulWidget { + const TextFieldSubSpaceDialogWidget({ + super.key, + required this.subSpaces, + }); + + final List subSpaces; + + @override + State createState() => _TextFieldSubSpaceDialogWidgetState(); +} + +class _TextFieldSubSpaceDialogWidgetState extends State { + + late final TextEditingController _subspaceNameController; + + @override + void initState() { + super.initState(); + _subspaceNameController = TextEditingController(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.name.toLowerCase(); + + final duplicateIndices = widget.subSpaces + .asMap() + .entries + .where((e) => e.value.name.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + onDeleted: () => context.read().add( + UpdateSpaceDetailsSubspaces( + widget.subSpaces.where((e) => e.uuid != subSpace.uuid).toList(), + ), + ), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + context.read().add( + UpdateSpaceDetailsSubspaces( + [ + ...widget.subSpaces, + Subspace( + name: trimmedValue, + uuid: '', + productAllocations: const [], + ), + ], + ), + ); + _subspaceNameController.clear(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart new file mode 100644 index 00000000..a80ddd15 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceChip extends StatelessWidget { + const SubspaceChip({ + required this.subSpace, + required this.isDuplicate, + required this.onDeleted, + super.key, + }); + + final Subspace subSpace; + final bool isDuplicate; + final void Function() onDeleted; + + @override + Widget build(BuildContext context) { + return Chip( + label: Text( + subSpace.name, + style: context.textTheme.bodySmall?.copyWith( + color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), + ), + onDeleted: onDeleted, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart new file mode 100644 index 00000000..3d7ab59b --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceNameDisplayWidget extends StatefulWidget { + const SubspaceNameDisplayWidget({super.key, required this.subSpace}); + + final Subspace subSpace; + + @override + State createState() => + _SubspaceNameDisplayWidgetState(); +} + +class _SubspaceNameDisplayWidgetState extends State { + late final TextEditingController _controller; + bool isEditing = false; + @override + void initState() { + _controller = TextEditingController(text: widget.subSpace.name); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.spaceColor, + ); + return InkWell( + onTap: () => setState(() => isEditing = true), + child: Visibility( + visible: isEditing, + replacement: Text( + widget.subSpace.name, + style: textStyle, + ), + child: TextField( + controller: _controller, + style: textStyle, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsetsDirectional.symmetric( + horizontal: 8, + ), + ), + onSubmitted: (value) { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid + ? e.copyWith(name: value) + : e, + ) + .toList(), + ), + ); + }, + ), + ), + ); + } +} From 9dfb3ed369dfdb11a71b5fb43f930209d7ef4f2e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:20:52 +0300 Subject: [PATCH 11/29] Refactor SpaceDetailsDialog and SpaceIconPicker to integrate Bloc for state management, enhancing icon selection and dialog functionality. --- .../views/space_management_page.dart | 2 - .../widgets/space_details_dialog.dart | 99 ++++++++++--------- .../widgets/space_icon_picker.dart | 22 ++++- .../widgets/space_icon_selection_dialog.dart | 31 ++++-- .../widgets/space_sub_spaces_box.dart | 6 +- .../space_details_model_bloc.dart | 4 +- 6 files changed, 99 insertions(+), 65 deletions(-) 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 c44236df..957be65a 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 @@ -7,7 +7,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s 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/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/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -19,7 +18,6 @@ class SpaceManagementPage extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => SpaceDetailsModelBloc()), BlocProvider( create: (context) => CommunitiesBloc( communitiesService: DebouncedCommunitiesService( diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 43b52ef1..fd3093c5 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' show SpaceDetailsModel; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; @@ -6,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/pres import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -23,56 +25,59 @@ class SpaceDetailsDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 1, - child: SpaceIconPicker( - iconPath: space.icon, - ), + return BlocProvider( + create: (context) => SpaceDetailsModelBloc(initialState: space), + child: Builder(builder: (context) { + final space = context.watch().state; + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: SpaceIconPicker(iconPath: space.icon) + ), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], ), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text(context.watch().state.toString()), - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, - ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), - ], - ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, ), ], - ), - ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], + ); + }), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart index 00552b88..fd6f79e6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.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'; @@ -39,10 +41,22 @@ class SpaceIconPicker extends StatelessWidget { top: 20, right: 20, child: InkWell( - onTap: () => showDialog( - context: context, - builder: (context) => const SpaceIconSelectionDialog(), - ), + onTap: () { + showDialog( + context: context, + builder: (context) => SpaceIconSelectionDialog( + selectedIcon: iconPath, + ), + ).then((value) { + if (value != null) { + if (context.mounted) { + context.read().add( + UpdateSpaceDetailsIcon(value), + ); + } + } + }); + }, child: Container( width: 24, height: 24, diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart index 428048b3..5fe5b463 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart @@ -5,7 +5,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceIconSelectionDialog extends StatelessWidget { - const SpaceIconSelectionDialog({super.key}); + const SpaceIconSelectionDialog({super.key, required this.selectedIcon}); + final String selectedIcon; static const List _icons = [ Assets.location, @@ -47,14 +48,26 @@ class SpaceIconSelectionDialog extends StatelessWidget { mainAxisSpacing: 16, ), itemCount: _icons.length, - itemBuilder: (context, index) => IconButton( - onPressed: Navigator.of(context).pop, - icon: SvgPicture.asset( - _icons[index], - width: context.screenWidth * 0.03, - height: context.screenHeight * 0.08, - ), - ), + itemBuilder: (context, index) { + final isSelected = selectedIcon == _icons[index]; + return Container( + padding: const EdgeInsetsDirectional.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isSelected + ? Border.all(color: ColorsManager.vividBlue, width: 2) + : null, + ), + child: IconButton( + onPressed: () => Navigator.of(context).pop(_icons[index]), + icon: SvgPicture.asset( + _icons[index], + width: context.screenWidth * 0.03, + height: context.screenHeight * 0.08, + ), + ), + ); + }, ), ), ); diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 071d6fec..30677e1e 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -30,7 +30,6 @@ class SpaceSubSpacesBox extends StatelessWidget { child: const ButtonContentWidget( svgAssets: Assets.addIcon, label: 'Create Sub Spaces', - // disabled: widget.isTagsAndSubspaceModelDisabled, disabled: false, ), ) @@ -55,7 +54,10 @@ class SpaceSubSpacesBox extends StatelessWidget { (e) => SubspaceNameDisplayWidget(subSpace: e), ), EditChip( - onTap: () {}, + onTap: () => showDialog( + context: context, + builder: (_) => const SpaceSubSpacesDialog(subspaces: []), + ), ), ], ), diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart index 57e794e8..21a72557 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -5,7 +5,9 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma part 'space_details_model_event.dart'; class SpaceDetailsModelBloc extends Bloc { - SpaceDetailsModelBloc() : super(SpaceDetailsModel.empty()) { + SpaceDetailsModelBloc({ + required SpaceDetailsModel initialState, + }) : super(initialState) { on(_onUpdateSpaceDetailsIcon); on(_onUpdateSpaceDetailsName); on(_onUpdateSpaceDetailsSubspaces); From e448eabda64cc1bec339f0a2068f7624caed6cee Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:41:13 +0300 Subject: [PATCH 12/29] Refactor SpaceSubSpacesDialog to use SubSpacesInput for managing subspaces, enhancing state management and UI structure. Update SpaceDetailsActionButtons to handle optional save callback. --- .../widgets/space_details_action_buttons.dart | 2 +- .../widgets/space_sub_spaces_dialog.dart | 179 ++++++------------ .../widgets/sub_spaces_input.dart | 103 ++++++++++ 3 files changed, 164 insertions(+), 120 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index 502ae4bb..3de9c192 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -11,7 +11,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { }); final VoidCallback onCancel; - final VoidCallback onSave; + final VoidCallback? onSave; @override Widget build(BuildContext context) { diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 1c435d8b..4230f7f4 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart'; +import 'package:uuid/uuid.dart'; -class SpaceSubSpacesDialog extends StatelessWidget { +class SpaceSubSpacesDialog extends StatefulWidget { const SpaceSubSpacesDialog({ required this.subspaces, super.key, @@ -15,129 +12,73 @@ class SpaceSubSpacesDialog extends StatelessWidget { final List subspaces; + @override + State createState() => _SpaceSubSpacesDialogState(); +} + +class _SpaceSubSpacesDialogState extends State { + late List _subspaces; + + bool get _hasDuplicateNames => + _subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length != + _subspaces.length; + + @override + void initState() { + super.initState(); + _subspaces = List.from(widget.subspaces); + } + + void _handleSubspaceAdded(String name) { + setState(() { + _subspaces = [ + ..._subspaces, + Subspace( + name: name, + uuid: const Uuid().v4(), + productAllocations: const [], + ), + ]; + }); + } + + void _handleSubspaceDeleted(String uuid) => setState( + () => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(), + ); + + void _handleSave() => Navigator.of(context).pop(_subspaces); + @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Create Sub Spaces'), - content: TextFieldSubSpaceDialogWidget( - subSpaces: subspaces, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SubSpacesInput( + subSpaces: _subspaces, + onSubspaceAdded: _handleSubspaceAdded, + onSubspaceDeleted: _handleSubspaceDeleted, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + child: Visibility( + key: ValueKey(_hasDuplicateNames), + visible: _hasDuplicateNames, + child: const Text( + 'Error: Duplicate subspace names are not allowed.', + style: TextStyle(color: Colors.red), + ), + ), + ), + ], ), actions: [ SpaceDetailsActionButtons( - onSave: () {}, + onSave: _hasDuplicateNames ? null : _handleSave, onCancel: Navigator.of(context).pop, ) ], ); } } - -class TextFieldSubSpaceDialogWidget extends StatefulWidget { - const TextFieldSubSpaceDialogWidget({ - super.key, - required this.subSpaces, - }); - - final List subSpaces; - - @override - State createState() => _TextFieldSubSpaceDialogWidgetState(); -} - -class _TextFieldSubSpaceDialogWidgetState extends State { - - late final TextEditingController _subspaceNameController; - - @override - void initState() { - super.initState(); - _subspaceNameController = TextEditingController(); - } - - @override - void dispose() { - _subspaceNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - width: context.screenWidth * 0.35, - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(10), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ...widget.subSpaces.asMap().entries.map( - (entry) { - final index = entry.key; - final subSpace = entry.value; - - final lowerName = subSpace.name.toLowerCase(); - - final duplicateIndices = widget.subSpaces - .asMap() - .entries - .where((e) => e.value.name.toLowerCase() == lowerName) - .map((e) => e.key) - .toList(); - final isDuplicate = duplicateIndices.length > 1 && - duplicateIndices.indexOf(index) != 0; - return SubspaceChip( - subSpace: subSpace, - isDuplicate: isDuplicate, - onDeleted: () => context.read().add( - UpdateSpaceDetailsSubspaces( - widget.subSpaces.where((e) => e.uuid != subSpace.uuid).toList(), - ), - ), - ); - }, - ), - SizedBox( - width: 200, - child: TextField( - controller: _subspaceNameController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, - hintStyle: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.lightGrayColor, - ), - ), - onSubmitted: (value) { - final trimmedValue = value.trim(); - if (trimmedValue.isNotEmpty) { - context.read().add( - UpdateSpaceDetailsSubspaces( - [ - ...widget.subSpaces, - Subspace( - name: trimmedValue, - uuid: '', - productAllocations: const [], - ), - ], - ), - ); - _subspaceNameController.clear(); - } - }, - style: context.textTheme.bodyMedium, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart new file mode 100644 index 00000000..1e1edb89 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpacesInput extends StatefulWidget { + const SubSpacesInput({ + super.key, + required this.subSpaces, + required this.onSubspaceAdded, + required this.onSubspaceDeleted, + }); + + final List subSpaces; + final void Function(String name) onSubspaceAdded; + final void Function(String uuid) onSubspaceDeleted; + + @override + State createState() => _SubSpacesInputState(); +} + +class _SubSpacesInputState extends State { + late final TextEditingController _subspaceNameController; + + @override + void initState() { + super.initState(); + _subspaceNameController = TextEditingController(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.name.toLowerCase(); + + final duplicateIndices = widget.subSpaces + .asMap() + .entries + .where((e) => e.value.name.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + widget.onSubspaceAdded(trimmedValue); + _subspaceNameController.clear(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} From 779c0fe9167430ee39e53f18d293b4ce5a969367 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:42:19 +0300 Subject: [PATCH 13/29] Refactor SpaceDetailsDialog to improve code readability and structure by simplifying widget hierarchy and enhancing the use of Bloc for state management. --- .../widgets/space_details_dialog.dart | 97 +++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index fd3093c5..ee6c7e7c 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -27,57 +27,56 @@ class SpaceDetailsDialog extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SpaceDetailsModelBloc(initialState: space), - child: Builder(builder: (context) { - final space = context.watch().state; - return AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 1, - child: SpaceIconPicker(iconPath: space.icon) - ), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, - ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), - ], + child: Builder( + builder: (context) { + final space = context.watch().state; + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: SpaceIconPicker(iconPath: space.icon)), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), ), - ), - ], + ], + ), ), - ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], - ); - }), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], + ); + }, + ), ); } } From 009b7c0316805f7c1a54362bb74f00d3281215b5 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:53:54 +0300 Subject: [PATCH 14/29] Refactor SpaceDetails feature to replace LoadSpacesParam with LoadSpaceDetailsParam, enhancing clarity in parameter handling. Introduce ClearSpaceDetails event in SpaceDetailsBloc for better state management. Update SpaceDetailsDialog and SpaceDetailsForm to utilize new parameter and improve dialog functionality. --- .../remote_space_details_service.dart | 4 +- .../params/load_space_details_param.dart | 7 + .../domain/params/load_spaces_param.dart | 3 - .../services/space_details_service.dart | 4 +- .../presentation/bloc/space_details_bloc.dart | 10 +- .../bloc/space_details_event.dart | 11 +- .../bloc/space_details_state.dart | 6 +- .../widgets/space_details_dialog.dart | 134 ++++++++++-------- .../widgets/space_details_form.dart | 76 ++++++++++ 9 files changed, 183 insertions(+), 72 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart delete mode 100644 lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart index 2e999361..17514e85 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -15,7 +15,7 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { static const _defaultErrorMessage = 'Failed to load space details'; @override - Future getSpaceDetails(LoadSpacesParam param) async { + Future getSpaceDetails(LoadSpaceDetailsParam param) async { try { final response = await _httpService.get( path: 'endpoint', diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart new file mode 100644 index 00000000..e5efbb83 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -0,0 +1,7 @@ +class LoadSpaceDetailsParam { + const LoadSpaceDetailsParam({ + this.spaceUuid, + }); + + final String? spaceUuid; +} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart deleted file mode 100644 index 5324ed98..00000000 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart +++ /dev/null @@ -1,3 +0,0 @@ -class LoadSpacesParam { - const LoadSpacesParam(); -} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart index b032560b..16b09ff1 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart @@ -1,6 +1,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; abstract class SpaceDetailsService { - Future getSpaceDetails(LoadSpacesParam param); + Future getSpaceDetails(LoadSpaceDetailsParam param); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart index 6eb96632..d397daf9 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -11,6 +11,7 @@ part 'space_details_state.dart'; class SpaceDetailsBloc extends Bloc { SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) { on(_onLoadSpaceDetails); + on(_onClearSpaceDetails); } final SpaceDetailsService _spaceDetailsService; @@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc { emit(SpaceDetailsFailure(e.toString())); } } + + void _onClearSpaceDetails( + ClearSpaceDetails event, + Emitter emit, + ) { + emit(SpaceDetailsInitial()); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart index fe559e26..9dd40fba 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart @@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable { List get props => []; } -class LoadSpaceDetails extends SpaceDetailsEvent { +final class LoadSpaceDetails extends SpaceDetailsEvent { const LoadSpaceDetails(this.param); - final LoadSpacesParam param; + final LoadSpaceDetailsParam param; @override List get props => [param]; } + +final class ClearSpaceDetails extends SpaceDetailsEvent { + const ClearSpaceDetails(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart index c7378f89..53c4cf77 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart @@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState { } final class SpaceDetailsFailure extends SpaceDetailsState { - final String message; + final String errorMessage; - const SpaceDetailsFailure(this.message); + const SpaceDetailsFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index ee6c7e7c..98b5027a 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' - show SpaceDetailsModel; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class SpaceDetailsDialog extends StatelessWidget { +class SpaceDetailsDialog extends StatefulWidget { const SpaceDetailsDialog({ required this.title, required this.space, @@ -24,58 +20,78 @@ class SpaceDetailsDialog extends StatelessWidget { final void Function(SpaceDetailsModel space) onSave; @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SpaceDetailsModelBloc(initialState: space), - child: Builder( - builder: (context) { - final space = context.watch().state; - return AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: SpaceIconPicker(iconPath: space.icon)), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, - ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), - ], - ), - ), - ], - ), + State createState() => _SpaceDetailsDialogState(); +} + +class _SpaceDetailsDialogState extends State { + @override + void initState() { + final isCreateMode = widget.space.uuid.isEmpty; + + if (!isCreateMode) { + context.read().add( + LoadSpaceDetails( + LoadSpaceDetailsParam(spaceUuid: widget.space.uuid), ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], ); - }, + } + super.initState(); + } + + @override + void deactivate() { + context.read().add(const ClearSpaceDetails()); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + final isCreateMode = widget.space.uuid.isEmpty; + if (isCreateMode) { + return SpaceDetailsForm( + title: widget.title, + space: widget.space, + onSave: widget.onSave, + ); + } + + return BlocBuilder( + builder: (context, state) => switch (state) { + SpaceDetailsInitial() => _buildLoadingDialog(), + SpaceDetailsLoading() => _buildLoadingDialog(), + SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm( + title: widget.title, + space: spaceDetails, + onSave: widget.onSave, + ), + SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog( + errorMessage, + ), + }, + ); + } + + Widget _buildLoadingDialog() { + return AlertDialog( + title: widget.title, + backgroundColor: ColorsManager.whiteColors, + content: const Center(child: CircularProgressIndicator()), + ); + } + + Widget _buildErrorDialog(String errorMessage) { + return AlertDialog( + title: widget.title, + backgroundColor: ColorsManager.whiteColors, + content: Center( + child: Text( + errorMessage, + style: context.textTheme.bodyLarge?.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), ), ); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart new file mode 100644 index 00000000..aa08d7fe --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceDetailsForm extends StatelessWidget { + const SpaceDetailsForm({ + required this.title, + required this.space, + required this.onSave, + super.key, + }); + + final Widget title; + final SpaceDetailsModel space; + final void Function(SpaceDetailsModel space) onSave; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SpaceDetailsModelBloc(initialState: space), + child: AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: SpaceIconPicker(iconPath: space.icon)), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], + ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], + ), + ); + } +} From 9990b1805e0c4c283113748c4e65f214748e6035 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 16:27:03 +0300 Subject: [PATCH 15/29] Fix typo in HomeBloc: change 'Devices Management' to 'Device Management' for consistency in naming. --- lib/pages/home/bloc/home_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index c1bcba6a..aa642e25 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -105,7 +105,7 @@ class HomeBloc extends Bloc { color: const Color(0xFF0026A2), ), HomeItemModel( - title: 'Devices Management', + title: 'Device Management', icon: Assets.devicesIcon, active: true, onPress: (context) { From c43cf9347f40e6a848b5fa1c982909a0a974cc07 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 16:29:02 +0300 Subject: [PATCH 16/29] Remove unnecessary deactivate method from SpaceDetailsDialog to streamline state management and improve code clarity. --- .../presentation/widgets/space_details_dialog.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 98b5027a..09355950 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -38,12 +38,6 @@ class _SpaceDetailsDialogState extends State { super.initState(); } - @override - void deactivate() { - context.read().add(const ClearSpaceDetails()); - super.deactivate(); - } - @override Widget build(BuildContext context) { final isCreateMode = widget.space.uuid.isEmpty; From 71cf4b9feb7c588feeafb8ee75d919142a4cc9dd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 16:30:23 +0300 Subject: [PATCH 17/29] Update LoadSpaceDetailsParam to require spaceUuid and refactor SpaceDetailsDialog to enhance clarity in parameter handling. --- .../domain/params/load_space_details_param.dart | 4 ++-- .../presentation/widgets/space_details_dialog.dart | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart index e5efbb83..c4c6c565 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -1,7 +1,7 @@ class LoadSpaceDetailsParam { const LoadSpaceDetailsParam({ - this.spaceUuid, + required this.spaceUuid, }); - final String? spaceUuid; + final String spaceUuid; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 09355950..69b54b4f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -29,11 +29,8 @@ class _SpaceDetailsDialogState extends State { final isCreateMode = widget.space.uuid.isEmpty; if (!isCreateMode) { - context.read().add( - LoadSpaceDetails( - LoadSpaceDetailsParam(spaceUuid: widget.space.uuid), - ), - ); + final param = LoadSpaceDetailsParam(spaceUuid: widget.space.uuid); + context.read().add(LoadSpaceDetails(param)); } super.initState(); } From c221c8499f69336c22d5ccae07fdd77a9f0a3f0d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 17:05:56 +0300 Subject: [PATCH 18/29] Add factory method `empty` to `SpaceModel` for default instance creation. Refactor SpaceDetailsDialog and related widgets to utilize SpaceModel, enhancing parameter handling and state management in space creation and editing flows. --- .../domain/models/space_model.dart | 10 +++ .../helpers/space_details_dialog_helper.dart | 13 +--- .../widgets/space_details_dialog.dart | 13 ++-- .../widgets/space_details_form.dart | 78 +++++++++---------- .../widgets/space_sub_spaces_box.dart | 27 +++++-- .../widgets/space_sub_spaces_dialog.dart | 7 +- .../widgets/subspace_name_display_widget.dart | 73 ++++++++++------- 7 files changed, 130 insertions(+), 91 deletions(-) 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 36943adb..ddcc6a86 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 @@ -19,6 +19,16 @@ class SpaceModel extends Equatable { required this.parent, }); + factory SpaceModel.empty() => const SpaceModel( + uuid: '', + createdAt: null, + updatedAt: null, + spaceName: '', + icon: '', + children: [], + parent: null, + ); + factory SpaceModel.fromJson(Map json) { return SpaceModel( uuid: json['uuid'] as String? ?? '', diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 5d6ffee7..723a5bc1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.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/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; abstract final class SpaceDetailsDialogHelper { @@ -9,8 +8,8 @@ abstract final class SpaceDetailsDialogHelper { context: context, builder: (_) => SpaceDetailsDialog( title: const Text('Create Space'), - space: SpaceDetailsModel.empty(), - onSave: (space) {}, + spaceModel: SpaceModel.empty(), + onSave: (space) => print(space), ), ); } @@ -23,13 +22,7 @@ abstract final class SpaceDetailsDialogHelper { context: context, builder: (_) => SpaceDetailsDialog( title: const Text('Edit Space'), - space: SpaceDetailsModel( - uuid: spaceModel.uuid, - spaceName: spaceModel.spaceName, - icon: spaceModel.icon, - productAllocations: const [], - subspaces: const [], - ), + spaceModel: spaceModel, onSave: (space) {}, ), ); diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 69b54b4f..85e9f009 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -10,13 +11,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDialog extends StatefulWidget { const SpaceDetailsDialog({ required this.title, - required this.space, + required this.spaceModel, required this.onSave, super.key, }); final Widget title; - final SpaceDetailsModel space; + final SpaceModel spaceModel; final void Function(SpaceDetailsModel space) onSave; @override @@ -26,10 +27,10 @@ class SpaceDetailsDialog extends StatefulWidget { class _SpaceDetailsDialogState extends State { @override void initState() { - final isCreateMode = widget.space.uuid.isEmpty; + final isCreateMode = widget.spaceModel.uuid.isEmpty; if (!isCreateMode) { - final param = LoadSpaceDetailsParam(spaceUuid: widget.space.uuid); + final param = LoadSpaceDetailsParam(spaceUuid: widget.spaceModel.uuid); context.read().add(LoadSpaceDetails(param)); } super.initState(); @@ -37,11 +38,11 @@ class _SpaceDetailsDialogState extends State { @override Widget build(BuildContext context) { - final isCreateMode = widget.space.uuid.isEmpty; + final isCreateMode = widget.spaceModel.uuid.isEmpty; if (isCreateMode) { return SpaceDetailsForm( title: widget.title, - space: widget.space, + space: SpaceDetailsModel.empty(), onSave: widget.onSave, ); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index aa08d7fe..24bae9a3 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -26,51 +26,51 @@ class SpaceDetailsForm extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SpaceDetailsModelBloc(initialState: space), - child: AlertDialog( - title: title, - backgroundColor: ColorsManager.whiteColors, - content: SizedBox( - height: context.screenHeight * 0.25, - child: Row( - spacing: 20, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: SpaceIconPicker(iconPath: space.icon)), - Expanded( - flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, + child: BlocBuilder( + buildWhen: (previous, current) => previous != current, + builder: (context, state) { + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.3, + child: Row( + spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SpaceNameTextField( - initialValue: space.spaceName, - isNameFieldExist: (value) { - final subspaces = space.subspaces; - if (subspaces.isEmpty) return false; - return subspaces.any( - (subspace) => subspace.name == value, - ); - }, + Expanded(child: SpaceIconPicker(iconPath: state.icon)), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpaceNameTextField( + initialValue: state.spaceName, + isNameFieldExist: (value) => state.subspaces.any( + (subspace) => subspace.name == value, + ), + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: state.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: state), + ], + ), ), - const Spacer(), - SpaceSubSpacesBox( - subspaces: space.subspaces, - ), - const SizedBox(height: 16), - SpaceDetailsDevicesBox(space: space), ], ), ), - ], - ), - ), - actions: [ - SpaceDetailsActionButtons( - onSave: () => onSave(space), - onCancel: Navigator.of(context).pop, - ), - ], - ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(state), + onCancel: Navigator.of(context).pop, + ), + ], + ); + }), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 30677e1e..5c3cef25 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.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'; @@ -23,10 +25,7 @@ class SpaceSubSpacesBox extends StatelessWidget { padding: EdgeInsets.zero, overlayColor: ColorsManager.transparentColor, ), - onPressed: () => showDialog( - context: context, - builder: (_) => SpaceSubSpacesDialog(subspaces: subspaces), - ), + onPressed: () => _showSubSpacesDialog(context), child: const ButtonContentWidget( svgAssets: Assets.addIcon, label: 'Create Sub Spaces', @@ -54,10 +53,7 @@ class SpaceSubSpacesBox extends StatelessWidget { (e) => SubspaceNameDisplayWidget(subSpace: e), ), EditChip( - onTap: () => showDialog( - context: context, - builder: (_) => const SpaceSubSpacesDialog(subspaces: []), - ), + onTap: () => _showSubSpacesDialog(context), ), ], ), @@ -66,4 +62,19 @@ class SpaceSubSpacesBox extends StatelessWidget { ], ); } + + void _showSubSpacesDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => SpaceSubSpacesDialog( + subspaces: subspaces, + onSave: (subspaces) { + context.read().add( + UpdateSpaceDetailsSubspaces(subspaces), + ); + }, + ), + ); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 4230f7f4..4c537a8a 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -7,10 +7,12 @@ import 'package:uuid/uuid.dart'; class SpaceSubSpacesDialog extends StatefulWidget { const SpaceSubSpacesDialog({ required this.subspaces, + required this.onSave, super.key, }); final List subspaces; + final void Function(List subspaces) onSave; @override State createState() => _SpaceSubSpacesDialogState(); @@ -46,7 +48,10 @@ class _SpaceSubSpacesDialogState extends State { () => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(), ); - void _handleSave() => Navigator.of(context).pop(_subspaces); + void _handleSave() { + widget.onSave(_subspaces); + Navigator.of(context).pop(); + } @override Widget build(BuildContext context) { diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index 3d7ab59b..9169efda 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -17,16 +17,19 @@ class SubspaceNameDisplayWidget extends StatefulWidget { class _SubspaceNameDisplayWidgetState extends State { late final TextEditingController _controller; + late final FocusNode _focusNode; bool isEditing = false; @override void initState() { _controller = TextEditingController(text: widget.subSpace.name); + _focusNode = FocusNode(); super.initState(); } @override void dispose() { _controller.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -36,38 +39,54 @@ class _SubspaceNameDisplayWidgetState extends State { color: ColorsManager.spaceColor, ); return InkWell( - onTap: () => setState(() => isEditing = true), - child: Visibility( - visible: isEditing, - replacement: Text( - widget.subSpace.name, - style: textStyle, + onTap: () { + setState(() => isEditing = true); + _focusNode.requestFocus(); + }, + child: Chip( + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), ), - child: TextField( - controller: _controller, - style: textStyle, - decoration: const InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsetsDirectional.symmetric( - horizontal: 8, + label: Visibility( + visible: isEditing, + replacement: Text( + widget.subSpace.name, + style: textStyle, + ), + child: SizedBox( + width: context.screenWidth * 0.065, + height: context.screenHeight * 0.025, + child: TextField( + focusNode: _focusNode, + controller: _controller, + style: textStyle, + decoration: const InputDecoration.collapsed(hintText: ''), + onTapOutside: (_) => _onFinishEditing(), + onSubmitted: (value) { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid + ? e.copyWith(name: value) + : e, + ) + .toList(), + ), + ); + _onFinishEditing(); + }, ), ), - onSubmitted: (value) { - final bloc = context.read(); - bloc.add( - UpdateSpaceDetailsSubspaces( - bloc.state.subspaces - .map( - (e) => e.uuid == widget.subSpace.uuid - ? e.copyWith(name: value) - : e, - ) - .toList(), - ), - ); - }, ), ), ); } + + void _onFinishEditing() { + setState(() => isEditing = false); + _focusNode.unfocus(); + } } From d47dc349bcd8cfafd7e3b6735c472cb071a42c8d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 12:04:03 +0300 Subject: [PATCH 19/29] Enhance SubspaceNameDisplayWidget to handle duplicate names during editing. Introduced logic to check for existing subspace names and provide user feedback. Refactored state management for editing and submission processes, improving overall user experience and code clarity. --- .../widgets/subspace_name_display_widget.dart | 108 +++++++++++++----- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index 9169efda..bbc9549d 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -18,7 +18,9 @@ class SubspaceNameDisplayWidget extends StatefulWidget { class _SubspaceNameDisplayWidgetState extends State { late final TextEditingController _controller; late final FocusNode _focusNode; - bool isEditing = false; + bool _isEditing = false; + bool _hasDuplicateName = false; + @override void initState() { _controller = TextEditingController(text: widget.subSpace.name); @@ -33,6 +35,41 @@ class _SubspaceNameDisplayWidgetState extends State { super.dispose(); } + bool _checkForDuplicateName(String name) { + final bloc = context.read(); + return bloc.state.subspaces + .where((s) => s.uuid != widget.subSpace.uuid) + .any((s) => s.name.toLowerCase() == name.toLowerCase()); + } + + void _handleNameChange(String value) { + setState(() { + _hasDuplicateName = _checkForDuplicateName(value); + }); + } + + void _tryToFinishEditing() { + if (!_hasDuplicateName) { + _onFinishEditing(); + } + } + + void _tryToSubmit(String value) { + if (_hasDuplicateName) return; + + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e, + ) + .toList(), + ), + ); + _onFinishEditing(); + } + @override Widget build(BuildContext context) { final textStyle = context.textTheme.bodySmall?.copyWith( @@ -40,7 +77,7 @@ class _SubspaceNameDisplayWidgetState extends State { ); return InkWell( onTap: () { - setState(() => isEditing = true); + setState(() => _isEditing = true); _focusNode.requestFocus(); }, child: Chip( @@ -49,36 +86,48 @@ class _SubspaceNameDisplayWidgetState extends State { borderRadius: BorderRadius.circular(10), ), label: Visibility( - visible: isEditing, + visible: _isEditing, replacement: Text( widget.subSpace.name, style: textStyle, ), - child: SizedBox( - width: context.screenWidth * 0.065, - height: context.screenHeight * 0.025, - child: TextField( - focusNode: _focusNode, - controller: _controller, - style: textStyle, - decoration: const InputDecoration.collapsed(hintText: ''), - onTapOutside: (_) => _onFinishEditing(), - onSubmitted: (value) { - final bloc = context.read(); - bloc.add( - UpdateSpaceDetailsSubspaces( - bloc.state.subspaces - .map( - (e) => e.uuid == widget.subSpace.uuid - ? e.copyWith(name: value) - : e, - ) - .toList(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: context.screenWidth * 0.065, + height: context.screenHeight * 0.025, + child: TextField( + focusNode: _focusNode, + controller: _controller, + style: textStyle?.copyWith( + color: _hasDuplicateName ? Colors.red : null, ), - ); - _onFinishEditing(); - }, - ), + decoration: const InputDecoration.collapsed( + hintText: '', + ), + onChanged: _handleNameChange, + onTapOutside: (_) => _tryToFinishEditing(), + onSubmitted: _tryToSubmit, + ), + ), + if (_hasDuplicateName) + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: Visibility( + key: ValueKey(_hasDuplicateName), + visible: _hasDuplicateName, + child: Text( + 'Name already exists', + style: textStyle?.copyWith( + color: Colors.red, + fontSize: 8, + ), + ), + ), + ), + ], ), ), ), @@ -86,7 +135,10 @@ class _SubspaceNameDisplayWidgetState extends State { } void _onFinishEditing() { - setState(() => isEditing = false); + setState(() { + _isEditing = false; + _hasDuplicateName = false; + }); _focusNode.unfocus(); } } From 318e1d9af7571658fd4a0e0cfa894557186b571d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 13:09:43 +0300 Subject: [PATCH 20/29] Implement Space Management Header and Action Buttons; integrate SpaceDetailsBloc for improved space management functionality. Add CommunityStructureHeader, CommunityStructureHeaderActionButtons, and CommunityStructureHeaderButton widgets to enhance UI and user interactions. Update SpaceManagementCommunityStructure to include the new header and refactor space details service for better endpoint handling. --- .../views/space_management_page.dart | 7 ++ .../widgets/community_structure_header.dart | 116 ++++++++++++++++++ ...unity_structure_header_action_buttons.dart | 46 +++++++ .../community_structure_header_button.dart | 61 +++++++++ .../space_management_community_structure.dart | 15 ++- .../remote_space_details_service.dart | 17 ++- .../params/load_space_details_param.dart | 2 + .../helpers/space_details_dialog_helper.dart | 32 +++-- .../widgets/space_details_dialog.dart | 14 ++- 9 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.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 957be65a..05768035 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 @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s 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/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/space_details/data/services/remote_space_details_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -26,6 +28,11 @@ class SpaceManagementPage extends StatelessWidget { )..add(const LoadCommunities(LoadCommunitiesParam())), ), BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), ], child: WebScaffold( appBarTitle: Text( diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart new file mode 100644 index 00000000..b457c413 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.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/create_community/presentation/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeader extends StatelessWidget { + const CommunityStructureHeader({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenWidth = MediaQuery.of(context).size.width; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.shadowBlackColor, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildCommunityInfo(context, theme, screenWidth), + ), + const SizedBox(width: 16), + ], + ), + ], + ), + ); + } + + void _showCreateCommunityDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + title: const Text('Edit Community'), + onCreateCommunity: (community) { + // TODO(FarisArmoush): Implement + }, + ), + ); + } + + Widget _buildCommunityInfo( + BuildContext context, ThemeData theme, double screenWidth) { + final selectedCommunity = + context.watch().state.selectedCommunity; + final selectedSpace = + context.watch().state.selectedSpace; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Community Structure', + style: theme.textTheme.headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + if (selectedCommunity != null) + Row( + children: [ + Expanded( + child: Row( + children: [ + Flexible( + child: SelectableText( + selectedCommunity.name, + style: theme.textTheme.bodyLarge + ?.copyWith(color: ColorsManager.blackColor), + maxLines: 1, + ), + ), + const SizedBox(width: 2), + GestureDetector( + onTap: () => _showCreateCommunityDialog(context), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + CommunityStructureHeaderActionButtons( + onDelete: (space) {}, + onDuplicate: (space) {}, + onEdit: (space) { + SpaceDetailsDialogHelper.showEdit( + context, + spaceModel: selectedSpace!, + ); + }, + selectedSpace: selectedSpace, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart new file mode 100644 index 00000000..a965c866 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeaderActionButtons extends StatelessWidget { + const CommunityStructureHeaderActionButtons({ + super.key, + required this.onDelete, + required this.selectedSpace, + required this.onDuplicate, + required this.onEdit, + }); + + final void Function(SpaceModel space) onDelete; + final void Function(SpaceModel space) onDuplicate; + final void Function(SpaceModel space) onEdit; + final SpaceModel? selectedSpace; + + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.end, + spacing: 10, + children: [ + if (selectedSpace != null) ...[ + CommunityStructureHeaderButton( + label: 'Edit', + svgAsset: Assets.editSpace, + onPressed: () => onEdit(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Duplicate', + svgAsset: Assets.duplicate, + onPressed: () => onDuplicate(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Delete', + svgAsset: Assets.spaceDelete, + onPressed: () => onDelete(selectedSpace!), + ), + ], + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart new file mode 100644 index 00000000..4c0285e3 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityStructureHeaderButton extends StatelessWidget { + const CommunityStructureHeaderButton({ + super.key, + required this.label, + required this.onPressed, + this.svgAsset, + }); + + final String label; + final VoidCallback onPressed; + final String? svgAsset; + + @override + Widget build(BuildContext context) { + const double buttonHeight = 40; + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 130, + minHeight: buttonHeight, + ), + child: DefaultButton( + onPressed: onPressed, + borderWidth: 2, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: ColorsManager.blackColor, + borderRadius: 12.0, + padding: 2.0, + height: buttonHeight, + elevation: 0, + borderColor: ColorsManager.lightGrayColor, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (svgAsset != null) + SvgPicture.asset( + svgAsset!, + width: 20, + height: 20, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + label, + style: context.textTheme.bodySmall + ?.copyWith(color: ColorsManager.blackColor, fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 99d0668a..e1f1fc00 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; @@ -18,9 +19,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget { replacement: const Row( children: [spacer, Expanded(child: CreateSpaceButton()), spacer], ), - child: CommunityStructureCanvas( - community: selectedCommunity, - selectedSpace: selectedSpace, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CommunityStructureHeader(), + Expanded( + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), + ), + ], ), ); } diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart index 17514e85..8b40cbfb 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; @@ -18,9 +19,12 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { Future getSpaceDetails(LoadSpaceDetailsParam param) async { try { final response = await _httpService.get( - path: 'endpoint', + path: await _makeEndpoint(param), expectedResponseModel: (data) { - return SpaceDetailsModel.fromJson(data as Map); + final response = data as Map; + return SpaceDetailsModel.fromJson( + response['data'] as Map, + ); }, ); return response; @@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { throw APIException(formattedErrorMessage); } } + + Future _makeEndpoint(LoadSpaceDetailsParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}'; + } } diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart index c4c6c565..7242e62e 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -1,7 +1,9 @@ class LoadSpaceDetailsParam { const LoadSpaceDetailsParam({ required this.spaceUuid, + required this.communityUuid, }); final String spaceUuid; + final String communityUuid; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 723a5bc1..c8835716 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,15 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/space_details/data/services/remote_space_details_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; abstract final class SpaceDetailsDialogHelper { static void showCreate(BuildContext context) { showDialog( context: context, - builder: (_) => SpaceDetailsDialog( - title: const Text('Create Space'), - spaceModel: SpaceModel.empty(), - onSave: (space) => print(space), + builder: (_) => BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + child: SpaceDetailsDialog( + context: context, + title: const Text('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: print, + ), ), ); } @@ -20,10 +30,16 @@ abstract final class SpaceDetailsDialogHelper { }) { showDialog( context: context, - builder: (_) => SpaceDetailsDialog( - title: const Text('Edit Space'), - spaceModel: spaceModel, - onSave: (space) {}, + builder: (_) => BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + child: SpaceDetailsDialog( + context: context, + title: const Text('Edit Space'), + spaceModel: spaceModel, + onSave: (space) {}, + ), ), ); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 85e9f009..abbd9aae 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -13,12 +14,14 @@ class SpaceDetailsDialog extends StatefulWidget { required this.title, required this.spaceModel, required this.onSave, + required this.context, super.key, }); final Widget title; final SpaceModel spaceModel; final void Function(SpaceDetailsModel space) onSave; + final BuildContext context; @override State createState() => _SpaceDetailsDialogState(); @@ -30,8 +33,15 @@ class _SpaceDetailsDialogState extends State { final isCreateMode = widget.spaceModel.uuid.isEmpty; if (!isCreateMode) { - final param = LoadSpaceDetailsParam(spaceUuid: widget.spaceModel.uuid); - context.read().add(LoadSpaceDetails(param)); + final param = LoadSpaceDetailsParam( + spaceUuid: widget.spaceModel.uuid, + communityUuid: widget.context + .read() + .state + .selectedCommunity! + .uuid, + ); + widget.context.read().add(LoadSpaceDetails(param)); } super.initState(); } From fc797c26462691d1bff82a162a7079c906a02fd3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 13:19:34 +0300 Subject: [PATCH 21/29] Refactor SpaceDetailsModel and ProductAllocation: Update JSON parsing for clarity and remove unused location field. Change subspace name mapping for consistency with API response. --- .../domain/models/space_details_model.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 8b9ff666..737d5060 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -31,8 +31,8 @@ class SpaceDetailsModel extends Equatable { spaceName: json['spaceName'] as String, icon: json['icon'] as String, productAllocations: (json['productAllocations'] as List) - .map((e) => ProductAllocation.fromJson(e as Map)) - .toList(), + .map((e) => ProductAllocation.fromJson(e as Map)) + .toList(), subspaces: (json['subspaces'] as List) .map((e) => Subspace.fromJson(e as Map)) .toList(), @@ -72,12 +72,10 @@ class SpaceDetailsModel extends Equatable { class ProductAllocation extends Equatable { final Product product; final Tag tag; - final String? location; const ProductAllocation({ required this.product, required this.tag, - this.location, }); factory ProductAllocation.fromJson(Map json) { @@ -97,12 +95,10 @@ class ProductAllocation extends Equatable { ProductAllocation copyWith({ Product? product, Tag? tag, - String? location, }) { return ProductAllocation( product: product ?? this.product, tag: tag ?? this.tag, - location: location ?? this.location, ); } @@ -124,7 +120,7 @@ class Subspace extends Equatable { factory Subspace.fromJson(Map json) { return Subspace( uuid: json['uuid'] as String, - name: json['name'] as String, + name: json['subspaceName'] as String, productAllocations: (json['productAllocations'] as List) .map((e) => ProductAllocation.fromJson(e as Map)) .toList(), From 9451ec0cc4da3322e3c24fd182a39592b555018b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 13:19:42 +0300 Subject: [PATCH 22/29] Update SpaceDetailsDialog to utilize SelectableText for error messages and ensure proper Bloc context usage. This enhances user experience by allowing text selection for easier copying of error information. --- .../presentation/widgets/space_details_dialog.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index abbd9aae..2461b3c6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -58,6 +58,7 @@ class _SpaceDetailsDialogState extends State { } return BlocBuilder( + bloc: widget.context.read(), builder: (context, state) => switch (state) { SpaceDetailsInitial() => _buildLoadingDialog(), SpaceDetailsLoading() => _buildLoadingDialog(), @@ -86,7 +87,7 @@ class _SpaceDetailsDialogState extends State { title: widget.title, backgroundColor: ColorsManager.whiteColors, content: Center( - child: Text( + child: SelectableText( errorMessage, style: context.textTheme.bodyLarge?.copyWith( color: ColorsManager.red, From 227df6fe3d74c455b34aecd6cd3d29db695e1095 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:23:00 +0300 Subject: [PATCH 23/29] Refactor SpaceDetailsWidgets: Simplify layout and improve responsiveness in SpaceDetailsDevicesBox and SpaceSubSpacesBox. Update SpaceDetailsForm to enhance dialog width for better user experience. This refactor enhances maintainability and aligns with Clean Architecture principles. --- .../widgets/space_details_devices_box.dart | 140 +++++++++--------- .../widgets/space_details_form.dart | 1 + .../widgets/space_sub_spaces_box.dart | 82 +++++----- 3 files changed, 107 insertions(+), 116 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 4873dc09..0208ccba 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.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'; class SpaceDetailsDevicesBox extends StatelessWidget { const SpaceDetailsDevicesBox({super.key, required this.space}); @@ -13,78 +12,75 @@ class SpaceDetailsDevicesBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - if (space.productAllocations.isNotEmpty || - space.subspaces - .any((subspace) => subspace.productAllocations.isNotEmpty)) - SizedBox( - width: context.screenWidth * 0.25, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width - ), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - // Combine tags from spaceModel and subspaces - // ...TagHelper.groupTags([ - // ...?tags, - // ...?subspaces?.expand((subspace) => subspace.tags ?? []) - // ]).entries.map( - // (entry) => Chip( - // avatar: SizedBox( - // width: 24, - // height: 24, - // child: SvgPicture.asset( - // entry.key.icon ?? 'assets/icons/gateway.svg', - // fit: BoxFit.contain, - // ), - // ), - // label: Text( - // 'x${entry.value}', // Show count - // style: Theme.of(context) - // .textTheme - // .bodySmall - // ?.copyWith(color: ColorsManager.spaceColor), - // ), - // backgroundColor: ColorsManager.whiteColors, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(16), - // side: const BorderSide( - // color: ColorsManager.spaceColor, - // ), - // ), - // ), - // ), + if (space.productAllocations.isNotEmpty || + space.subspaces.any((subspace) => subspace.productAllocations.isNotEmpty)) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + // ...TagHelper.groupTags([ + // ...?tags, + // ...?subspaces?.expand((subspace) => subspace.tags ?? []) + // ]).entries.map( + // (entry) => Chip( + // avatar: SizedBox( + // width: 24, + // height: 24, + // child: SvgPicture.asset( + // entry.key.icon ?? 'assets/icons/gateway.svg', + // fit: BoxFit.contain, + // ), + // ), + // label: Text( + // 'x${entry.value}', // Show count + // style: Theme.of(context) + // .textTheme + // .bodySmall + // ?.copyWith(color: ColorsManager.spaceColor), + // ), + // backgroundColor: ColorsManager.whiteColors, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // side: const BorderSide( + // color: ColorsManager.spaceColor, + // ), + // ), + // ), + // ), - EditChip( - onTap: () {}, - ), - ], - ), + EditChip( + onTap: () {}, ), - ) - else - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - ), - child: const ButtonContentWidget( - svgAssets: Assets.addIcon, - label: 'Add Devices', - // disabled: isTagsAndSubspaceModelDisabled, - ), - ) - ], - ); + ], + ), + ); + } else { + return TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const SizedBox( + width: double.infinity, + child: ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Add Devices', + // disabled: isTagsAndSubspaceModelDisabled, + ), + ), + ); + } } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index 24bae9a3..f9a5ad64 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -34,6 +34,7 @@ class SpaceDetailsForm extends StatelessWidget { backgroundColor: ColorsManager.whiteColors, content: SizedBox( height: context.screenHeight * 0.3, + width: context.screenWidth * 0.5, child: Row( spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 5c3cef25..68bf68bd 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -8,7 +8,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/pres import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.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'; class SpaceSubSpacesBox extends StatelessWidget { const SpaceSubSpacesBox({super.key, required this.subspaces}); @@ -17,50 +16,45 @@ class SpaceSubSpacesBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - if (subspaces.isEmpty) - TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - overlayColor: ColorsManager.transparentColor, - ), - onPressed: () => _showSubSpacesDialog(context), - child: const ButtonContentWidget( - svgAssets: Assets.addIcon, - label: 'Create Sub Spaces', - disabled: false, - ), - ) - else - SizedBox( - width: context.screenWidth * 0.25, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: ColorsManager.textFieldGreyColor, - width: 3.0, - ), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - ...subspaces.map( - (e) => SubspaceNameDisplayWidget(subSpace: e), - ), - EditChip( - onTap: () => _showSubSpacesDialog(context), - ), - ], - ), - ), + if (subspaces.isEmpty) { + return TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () => _showSubSpacesDialog(context), + child: const SizedBox( + width: double.infinity, + child: ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Create Sub Spaces', ), - ], - ); + ), + ); + } else { + return Container( + padding: const EdgeInsets.all(8.0), + width: double.infinity, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)), + EditChip( + onTap: () => _showSubSpacesDialog(context), + ), + ], + ), + ); + } } void _showSubSpacesDialog(BuildContext context) { From 58e99f95b20c1cb711700220fc643f8bee38a89b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:25:04 +0300 Subject: [PATCH 24/29] removed comments. --- .../presentation/widgets/space_details_devices_box.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 0208ccba..6def57d9 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -22,7 +22,7 @@ class SpaceDetailsDevicesBox extends StatelessWidget { borderRadius: BorderRadius.circular(15), border: Border.all( color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width + width: 3.0, ), ), child: Wrap( @@ -44,7 +44,7 @@ class SpaceDetailsDevicesBox extends StatelessWidget { // ), // ), // label: Text( - // 'x${entry.value}', // Show count + // 'x${entry.value}', // style: Theme.of(context) // .textTheme // .bodySmall From c07ddb0ccdd967826c45f0ca8d64a030d75ee86f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:27:06 +0300 Subject: [PATCH 25/29] Refactor SpaceDetailsDevicesBox: Improve readability by extracting variables for product allocations and subspaces. This change enhances code clarity and maintainability in line with Clean Architecture principles. --- .../widgets/space_details_devices_box.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 6def57d9..bfd68b9e 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -6,14 +6,20 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class SpaceDetailsDevicesBox extends StatelessWidget { - const SpaceDetailsDevicesBox({super.key, required this.space}); + const SpaceDetailsDevicesBox({ + required this.space, + super.key, + }); final SpaceDetailsModel space; @override Widget build(BuildContext context) { - if (space.productAllocations.isNotEmpty || - space.subspaces.any((subspace) => subspace.productAllocations.isNotEmpty)) { + final productAllocations = space.productAllocations; + final subspaces = space.subspaces; + final isAnySubspaceHasProductAllocations = + subspaces.any((subspace) => subspace.productAllocations.isNotEmpty); + if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) { return Container( width: double.infinity, padding: const EdgeInsets.all(8.0), From 1fccd51440c020803e2cbcd1021dbce1bd5dbde3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:33:34 +0300 Subject: [PATCH 26/29] Refactor SubspaceNameDisplayWidget: Update Chip border radius for improved aesthetics and add delete functionality to remove subspaces. This enhances user interaction and aligns with maintainability principles. --- .../widgets/space_details_devices_box.dart | 2 +- .../widgets/subspace_name_display_widget.dart | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index bfd68b9e..5b21bd61 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -22,7 +22,7 @@ class SpaceDetailsDevicesBox extends StatelessWidget { if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) { return Container( width: double.infinity, - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: ColorsManager.textFieldGreyColor, borderRadius: BorderRadius.circular(15), diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index bbc9549d..bf13ffd3 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -83,7 +83,34 @@ class _SubspaceNameDisplayWidgetState extends State { child: Chip( backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: ColorsManager.transparentColor), + ), + onDeleted: () { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .where((s) => s.uuid != widget.subSpace.uuid) + .toList(), + ), + ); + }, + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), ), label: Visibility( visible: _isEditing, From b857736e10abe1c1eeb40ba10f7fe0f5cc38924a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 15:37:06 +0300 Subject: [PATCH 27/29] Refactor SpaceNameTextField: Update text styling and border handling to utilize context-based theming. This improves consistency with the app's theme and enhances maintainability by centralizing border styling logic. --- .../widgets/space_name_text_field.dart | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart index d5bd1016..0c62490f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart @@ -56,7 +56,7 @@ class _SpaceNameTextFieldState extends State { UpdateSpaceDetailsName(value), ), validator: _validateName, - style: Theme.of(context).textTheme.bodyMedium, + style: context.textTheme.bodyMedium, decoration: InputDecoration( hintText: 'Please enter the name', hintStyle: context.textTheme.bodyMedium!.copyWith( @@ -64,21 +64,22 @@ class _SpaceNameTextFieldState extends State { ), filled: true, fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(width: 1.5), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - ), - ), + enabledBorder: _buildBorder(context, ColorsManager.vividBlue), + focusedBorder: _buildBorder(context, ColorsManager.primaryColor), + errorBorder: _buildBorder(context, context.theme.colorScheme.error), + focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error), errorStyle: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.red, + color: context.theme.colorScheme.error, ), ), ), ); } + + OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor), + ); + } } From 757a96ed9f6d5e3044cbe7bc5d539f86bd48abc3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 3 Jul 2025 16:15:31 +0300 Subject: [PATCH 28/29] Refactor SpaceDetailsActionButtons and SpaceIconPicker: Adjust layout properties for improved responsiveness and user experience. Set mainAxisSize to min in SpaceDetailsActionButtons and simplify the layout in SpaceIconPicker for better alignment and interaction. This enhances maintainability and adheres to Clean Architecture principles. --- .../widgets/space_details_action_buttons.dart | 1 + .../widgets/space_icon_picker.dart | 98 +++++++++---------- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index 3de9c192..8518227f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -16,6 +16,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { return Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, spacing: 10, children: [ diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart index fd6f79e6..68bfdefc 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -17,64 +17,60 @@ class SpaceIconPicker extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 50), - Stack( - alignment: Alignment.center, - children: [ - Container( - width: context.screenWidth * 0.1, - height: context.screenWidth * 0.1, - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - shape: BoxShape.circle, - ), + return Center( + child: Stack( + + alignment: Alignment.center, + children: [ + Container( + width: context.screenWidth * 0.175, + height: context.screenHeight * 0.175, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, ), - SvgPicture.asset( + padding: const EdgeInsets.all(24), + child: SvgPicture.asset( iconPath, - width: context.screenWidth * 0.04, - height: context.screenWidth * 0.04, + width: context.screenWidth * 0.08, + height: context.screenHeight * 0.08, ), - Positioned( - top: 20, - right: 20, - child: InkWell( - onTap: () { - showDialog( - context: context, - builder: (context) => SpaceIconSelectionDialog( - selectedIcon: iconPath, - ), - ).then((value) { - if (value != null) { - if (context.mounted) { - context.read().add( - UpdateSpaceDetailsIcon(value), - ); - } + ), + Positioned.directional( + top: 12, + start: context.screenHeight * 0.06, + textDirection: Directionality.of(context), + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) => SpaceIconSelectionDialog( + selectedIcon: iconPath, + ), + ).then((value) { + if (value != null) { + if (context.mounted) { + context.read().add( + UpdateSpaceDetailsIcon(value), + ); } - }); - }, - child: Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: SvgPicture.asset( - Assets.iconEdit, - width: 16, - height: 16, - ), + } + }); + }, + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, ), ), ), - ], - ), - ], + ), + ], + ), ); } } From 95cded4bf5e0143ca4e28e4e3897250051a560b0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 09:04:13 +0300 Subject: [PATCH 29/29] Enhance SubSpacesInput: Introduce FocusNode for improved text field focus management. This change allows the input field to regain focus after adding a subspace, enhancing user experience and maintaining clean state management practices. --- .../presentation/widgets/sub_spaces_input.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart index 1e1edb89..854b79bc 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -22,16 +22,18 @@ class SubSpacesInput extends StatefulWidget { class _SubSpacesInputState extends State { late final TextEditingController _subspaceNameController; - + late final FocusNode _focusNode; @override void initState() { super.initState(); _subspaceNameController = TextEditingController(); + _focusNode = FocusNode(); } @override void dispose() { _subspaceNameController.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -78,6 +80,7 @@ class _SubSpacesInputState extends State { SizedBox( width: 200, child: TextField( + focusNode: _focusNode, controller: _subspaceNameController, decoration: InputDecoration( border: InputBorder.none, @@ -91,6 +94,7 @@ class _SubSpacesInputState extends State { if (trimmedValue.isNotEmpty) { widget.onSubspaceAdded(trimmedValue); _subspaceNameController.clear(); + _focusNode.requestFocus(); } }, style: context.textTheme.bodyMedium,