From 4907eebc42a46199b70e721b58b8b52aefbc0f8d Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 27 Jan 2025 00:33:50 +0400 Subject: [PATCH] added duplicate --- assets/icons/duplicate.svg | 16 + assets/icons/space_delete.svg | 9 + .../bloc/add_device_model_bloc.dart | 86 +++-- .../bloc/add_device_state.dart | 36 ++ .../bloc/add_device_type_model_event.dart | 24 +- .../views/add_device_type_widget.dart | 141 ++++--- .../widgets/scrollable_grid_view_widget.dart | 8 +- ...munity_structure_header_action_button.dart | 31 +- .../community_structure_header_button.dart | 8 +- .../community_structure_header_widget.dart | 9 +- .../widgets/community_structure_widget.dart | 177 ++++++++- .../widgets/dialogs/create_space_dialog.dart | 136 +++---- .../dialogs/duplicate_process_dialog.dart | 86 +++++ .../assign_tag/bloc/assign_tag_bloc.dart | 12 +- .../assign_tag/views/assign_tag_dialog.dart | 352 ++++++++++-------- .../views/assign_tag_models_dialog.dart | 2 +- .../create_subspace/bloc/subspace_bloc.dart | 48 ++- .../bloc/subspace_model_bloc.dart | 54 ++- .../spaces_management/helper/tag_helper.dart | 68 +++- .../bloc/create_space_model_bloc.dart | 173 +++++---- lib/utils/color_manager.dart | 2 +- lib/utils/constants/assets.dart | 2 + 22 files changed, 1025 insertions(+), 455 deletions(-) create mode 100644 assets/icons/duplicate.svg create mode 100644 assets/icons/space_delete.svg create mode 100644 lib/pages/spaces_management/add_device_type/bloc/add_device_state.dart create mode 100644 lib/pages/spaces_management/all_spaces/widgets/dialogs/duplicate_process_dialog.dart diff --git a/assets/icons/duplicate.svg b/assets/icons/duplicate.svg new file mode 100644 index 00000000..1faa1bab --- /dev/null +++ b/assets/icons/duplicate.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/space_delete.svg b/assets/icons/space_delete.svg new file mode 100644 index 00000000..90c3413e --- /dev/null +++ b/assets/icons/space_delete.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart b/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart index 10f3327e..e84851c5 100644 --- a/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart +++ b/lib/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart @@ -1,38 +1,74 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart'; -class AddDeviceTypeBloc - extends Bloc> { - AddDeviceTypeBloc(List initialProducts) - : super(initialProducts) { +class AddDeviceTypeBloc extends Bloc { + AddDeviceTypeBloc() : super(AddDeviceInitial()) { + on(_onInitializeTagModels); on(_onUpdateProductCount); } - void _onUpdateProductCount( - UpdateProductCountEvent event, Emitter> emit) { - final existingProduct = state.firstWhere( - (p) => p.productId == event.productId, - orElse: () => SelectedProduct(productId: event.productId, count: 0,productName: event.productName,product: event.product ), - ); + void _onInitializeTagModels( + InitializeDevice event, Emitter emit) { + emit(AddDeviceLoaded( + selectedProducts: event.addedProducts, + initialTag: event.initialTags, + )); + } - if (event.count > 0) { - if (!state.contains(existingProduct)) { - emit([ - ...state, - SelectedProduct(productId: event.productId, count: event.count, productName: event.productName, product: event.product) - ]); + void _onUpdateProductCount( + UpdateProductCountEvent event, Emitter emit) { + final currentState = state; + + if (currentState is AddDeviceLoaded) { + final existingProduct = currentState.selectedProducts.firstWhere( + (p) => p.productId == event.productId, + orElse: () => SelectedProduct( + productId: event.productId, + count: 0, + productName: event.productName, + product: event.product, + ), + ); + + List updatedProducts; + + if (event.count > 0) { + if (!currentState.selectedProducts.contains(existingProduct)) { + updatedProducts = [ + ...currentState.selectedProducts, + SelectedProduct( + productId: event.productId, + count: event.count, + productName: event.productName, + product: event.product, + ), + ]; + } else { + updatedProducts = currentState.selectedProducts.map((p) { + if (p.productId == event.productId) { + return SelectedProduct( + productId: p.productId, + count: event.count, + productName: p.productName, + product: p.product, + ); + } + return p; + }).toList(); + } } else { - final updatedList = state.map((p) { - if (p.productId == event.productId) { - return SelectedProduct(productId: p.productId, count: event.count, productName: p.productName,product: p.product); - } - return p; - }).toList(); - emit(updatedList); + // Remove the product if the count is 0 + updatedProducts = currentState.selectedProducts + .where((p) => p.productId != event.productId) + .toList(); } - } else { - emit(state.where((p) => p.productId != event.productId).toList()); + + // Emit the updated state + emit(AddDeviceLoaded( + selectedProducts: updatedProducts, + initialTag: currentState.initialTag)); } } } diff --git a/lib/pages/spaces_management/add_device_type/bloc/add_device_state.dart b/lib/pages/spaces_management/add_device_type/bloc/add_device_state.dart new file mode 100644 index 00000000..e1fa2593 --- /dev/null +++ b/lib/pages/spaces_management/add_device_type/bloc/add_device_state.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; + +abstract class AddDeviceState extends Equatable { + const AddDeviceState(); + + @override + List get props => []; +} + +class AddDeviceInitial extends AddDeviceState {} + +class AddDeviceLoading extends AddDeviceState {} + +class AddDeviceLoaded extends AddDeviceState { + final List selectedProducts; + final List initialTag; + + const AddDeviceLoaded({ + required this.selectedProducts, + required this.initialTag, + }); + + @override + List get props => [selectedProducts, initialTag]; +} + +class AddDeviceError extends AddDeviceState { + final String errorMessage; + + const AddDeviceError(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart b/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart index addb6d67..254b78fd 100644 --- a/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart +++ b/lib/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart @@ -1,7 +1,11 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; abstract class AddDeviceTypeEvent extends Equatable { + const AddDeviceTypeEvent(); + @override List get props => []; } @@ -12,8 +16,26 @@ class UpdateProductCountEvent extends AddDeviceTypeEvent { final String productName; final ProductModel product; - UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product}); + UpdateProductCountEvent( + {required this.productId, + required this.count, + required this.productName, + required this.product}); @override List get props => [productId, count]; } + + +class InitializeDevice extends AddDeviceTypeEvent { + final List initialTags; + final List addedProducts; + + const InitializeDevice({ + this.initialTags = const [], + required this.addedProducts, + }); + + @override + List get props => [initialTags, addedProducts]; +} diff --git a/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart b/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart index 9b9d6886..b26dbcc7 100644 --- a/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart +++ b/lib/pages/spaces_management/add_device_type/views/add_device_type_widget.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart'; import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/action_button_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; - class AddDeviceTypeWidget extends StatelessWidget { final List? products; final ValueChanged>? onProductsSelected; @@ -20,8 +23,7 @@ class AddDeviceTypeWidget extends StatelessWidget { final List? spaceTags; final List? allTags; final String spaceName; - final Function(List,List?)? onSave; - + final Function(List, List?)? onSave; const AddDeviceTypeWidget( {super.key, @@ -44,30 +46,45 @@ class AddDeviceTypeWidget extends StatelessWidget { : 3; return BlocProvider( - create: (_) => AddDeviceTypeBloc(initialSelectedProducts ?? []), + create: (_) => AddDeviceTypeBloc() + ..add(InitializeDevice( + initialTags: spaceTags ?? [], + addedProducts: initialSelectedProducts ?? [], + )), child: Builder( builder: (context) => AlertDialog( title: const Text('Add Devices'), backgroundColor: ColorsManager.whiteColors, - content: SingleChildScrollView( - child: Container( - width: size.width * 0.9, - height: size.height * 0.65, - color: ColorsManager.textFieldGreyColor, - child: Column( - children: [ - const SizedBox(height: 16), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: ScrollableGridViewWidget( - products: products, crossAxisCount: crossAxisCount), - ), + content: BlocBuilder( + builder: (context, state) { + if (state is AddDeviceLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is AddDeviceLoaded) { + return SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + color: ColorsManager.textFieldGreyColor, + child: Column( + children: [ + const SizedBox(height: 16), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20.0), + child: ScrollableGridViewWidget( + products: products, + crossAxisCount: crossAxisCount), + ), + ), + ], ), - ], - ), - ), - ), + ), + ); + } + return const SizedBox(); + }), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -78,43 +95,53 @@ class AddDeviceTypeWidget extends StatelessWidget { Navigator.of(context).pop(); }, ), - ActionButton( - label: 'Continue', - backgroundColor: ColorsManager.secondaryColor, - foregroundColor: ColorsManager.whiteColors, - onPressed: () async { - final currentState = - context.read().state; - Navigator.of(context).pop(); + SizedBox( + width: 140, + child: BlocBuilder( + builder: (context, state) { + final isDisabled = state is AddDeviceLoaded && + state.selectedProducts.isEmpty; + return DefaultButton( + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: isDisabled + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, + onPressed: () async { + if (state is AddDeviceLoaded && + state.selectedProducts.isNotEmpty) { + final initialTags = + TagHelper.generateInitialForTags( + spaceTags: spaceTags, + subspaces: subspaces, + ); + Navigator.of(context).pop(); - if (currentState.isNotEmpty) { - final initialTags = generateInitialTags( - spaceTags: spaceTags, - subspaces: subspaces, - ); - - final dialogTitle = initialTags.isNotEmpty - ? 'Edit Device' - : 'Assign Tags'; - await showDialog( - barrierDismissible: false, - context: context, - builder: (context) => AssignTagDialog( - products: products, - subspaces: subspaces, - addedProducts: currentState, - allTags: allTags, - spaceName: spaceName, - initialTags: initialTags, - title: dialogTitle, - onSave: (tags,subspaces){ - onSave!(tags,subspaces); + final dialogTitle = initialTags.isNotEmpty + ? 'Edit Device' + : 'Assign Tags'; + await showDialog( + barrierDismissible: false, + context: context, + builder: (context) => AssignTagDialog( + products: products, + subspaces: subspaces, + addedProducts: state.selectedProducts, + allTags: allTags, + spaceName: spaceName, + initialTags: initialTags, + title: dialogTitle, + onSave: (tags, subspaces) { + onSave!(tags, subspaces); + }, + ), + ); + } }, - ), - ); - } - }, - ), + child: const Text('Next'), + ); + }, + )), ], ), ], diff --git a/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart b/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart index aeee6f1b..97bcf6d1 100644 --- a/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart +++ b/lib/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.dart'; import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; @@ -24,8 +25,11 @@ class ScrollableGridViewWidget extends StatelessWidget { return Scrollbar( controller: scrollController, thumbVisibility: true, - child: BlocBuilder>( - builder: (context, productCounts) { + child: BlocBuilder( + builder: (context, state) { + final productCounts = state is AddDeviceLoaded + ? state.selectedProducts + : []; return GridView.builder( controller: scrollController, shrinkWrap: true, diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart index 66bfe943..f5188c1e 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart @@ -5,19 +5,23 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class CommunityStructureHeaderActionButtons extends StatelessWidget { - const CommunityStructureHeaderActionButtons({ - super.key, - required this.theme, - required this.isSave, - required this.onSave, - required this.onDelete, - required this.selectedSpace, - }); + const CommunityStructureHeaderActionButtons( + {super.key, + required this.theme, + required this.isSave, + required this.onSave, + required this.onDelete, + required this.selectedSpace, + required this.onDuplicate, + required this.onEdit}); final ThemeData theme; final bool isSave; final VoidCallback onSave; final VoidCallback onDelete; + final VoidCallback onDuplicate; + final VoidCallback onEdit; + final SpaceModel? selectedSpace; @override @@ -42,13 +46,18 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget { CommunityStructureHeaderButton( label: "Edit", svgAsset: Assets.editSpace, - onPressed: () => {}, + onPressed: onEdit, + theme: theme, + ), + CommunityStructureHeaderButton( + label: "Duplicate", + svgAsset: Assets.duplicate, + onPressed: onDuplicate, theme: theme, ), CommunityStructureHeaderButton( label: "Delete", - icon: const Icon(Icons.delete, - size: 18, color: ColorsManager.warningRed), + svgAsset: Assets.spaceDelete, onPressed: onDelete, theme: theme, ), diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart index 4388c965..0369f7cd 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart @@ -24,12 +24,12 @@ class CommunityStructureHeaderButton extends StatelessWidget { const double buttonHeight = 40; return ConstrainedBox( constraints: const BoxConstraints( - maxWidth: 100, + maxWidth: 130, minHeight: buttonHeight, ), child: DefaultButton( onPressed: onPressed, - borderWidth: 3, + borderWidth: 2, backgroundColor: ColorsManager.textFieldGreyColor, foregroundColor: ColorsManager.blackColor, borderRadius: 12.0, @@ -44,8 +44,8 @@ class CommunityStructureHeaderButton extends StatelessWidget { if (svgAsset != null) SvgPicture.asset( svgAsset!, - width: 30, - height: 30, + width: 20, + height: 20, ), const SizedBox(width: 10), Flexible( diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart index 0419dc84..6bc35cca 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart @@ -14,6 +14,9 @@ class CommunityStructureHeader extends StatefulWidget { final TextEditingController nameController; final VoidCallback onSave; final VoidCallback onDelete; + final VoidCallback onEdit; + final VoidCallback onDuplicate; + final VoidCallback onEditName; final ValueChanged onNameSubmitted; final List communities; @@ -32,7 +35,9 @@ class CommunityStructureHeader extends StatefulWidget { required this.onNameSubmitted, this.community, required this.communities, - this.selectedSpace}); + this.selectedSpace, + required this.onDuplicate, + required this.onEdit}); @override State createState() => @@ -146,6 +151,8 @@ class _CommunityStructureHeaderState extends State { isSave: widget.isSave, onSave: widget.onSave, onDelete: widget.onDelete, + onDuplicate: widget.onDuplicate, + onEdit: widget.onEdit, selectedSpace: widget.selectedSpace, ), ], diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart index 8c6e36f5..b5f54708 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart @@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; // Syncrow project imports import 'package:syncrow_web/pages/common/buttons/add_space_button.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; @@ -17,6 +19,7 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_com import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/duplicate_process_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; @@ -133,6 +136,8 @@ class _CommunityStructureAreaState extends State { onSave: _saveSpaces, selectedSpace: widget.selectedSpace, onDelete: _onDelete, + onDuplicate: () => {_onDuplicate(context)}, + onEdit: () => {}, onEditName: () { setState(() { isEditingName = !isEditingName; @@ -328,7 +333,6 @@ class _CommunityStructureAreaState extends State { parentSpace.addOutgoingConnection(newConnection); parentSpace.children.add(newSpace); } - spaces.add(newSpace); _updateNodePosition(newSpace, newSpace.position); }); @@ -546,4 +550,175 @@ class _CommunityStructureAreaState extends State { space.status == SpaceStatus.modified || space.status == SpaceStatus.deleted); } + + void _onDuplicate(BuildContext parentContext) { + final screenWidth = MediaQuery.of(context).size.width; + + if (widget.selectedSpace != null) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: ColorsManager.whiteColors, + title: Text( + "Duplicate Space", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + content: SizedBox( + width: screenWidth * 0.4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Are you sure you want to duplicate?", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 15), + Text("All the child spaces will be duplicated.", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: ColorsManager.lightGrayColor)), + const SizedBox(width: 15), + ], + ), + ), + actions: [ + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + SizedBox( + width: 200, + child: CancelButton( + onPressed: () { + Navigator.of(context).pop(); + }, + label: "Cancel", + ), + ), + const SizedBox(width: 10), + SizedBox( + width: 200, + child: DefaultButton( + onPressed: () { + Navigator.of(context).pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return DuplicateProcessDialog( + onDuplicate: () { + _duplicateSpace(widget.selectedSpace!); + _deselectSpace(parentContext); + }, + ); + }, + ); + }, + backgroundColor: ColorsManager.secondaryColor, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ]) + ], + ); + }, + ); + } + } + + void _duplicateSpace(SpaceModel space) { + final Map originalToDuplicate = {}; + const double horizontalGap = 150.0; + const double verticalGap = 100.0; + + SpaceModel duplicateRecursive(SpaceModel original, Offset parentPosition) { + // Find a new position for the duplicated space + Offset newPosition = parentPosition + Offset(horizontalGap, 0); + + // Avoid overlapping with existing spaces + while (spaces.any((s) => + (s.position - newPosition).distance < horizontalGap && + s.status != SpaceStatus.deleted)) { + newPosition += Offset(horizontalGap, 0); + } + + // Create the duplicated space + final duplicated = SpaceModel( + name: "${original.name} (Copy)", + icon: original.icon, + position: newPosition, + isPrivate: original.isPrivate, + children: [], + status: SpaceStatus.newSpace, + parent: original.parent, + spaceModel: original.spaceModel, + subspaces: original.subspaces, + tags: original.tags, + ); + + originalToDuplicate[original] = duplicated; + + // Copy the children of the original space to the duplicated space + Offset childStartPosition = newPosition + Offset(0, verticalGap); + for (final child in original.children) { + final duplicatedChild = duplicateRecursive(child, childStartPosition); + duplicated.children.add(duplicatedChild); + duplicatedChild.parent = + duplicated; // Set the parent for the duplicated child + childStartPosition += Offset(0, verticalGap); + } + + return duplicated; + } + + // Duplicate the selected space and its children + final duplicatedSpace = duplicateRecursive(space, space.position); + + // Ensure the duplicated space has the same parent as the original + if (space.parent != null) { + final parentSpace = space.parent!; + final duplicatedParent = originalToDuplicate[parentSpace] ?? parentSpace; + duplicatedSpace.parent = duplicatedParent; + duplicatedParent.children.add(duplicatedSpace); + } + + // Flatten the hierarchy of the duplicated spaces + List flattenHierarchy(SpaceModel root) { + final List result = []; + void traverse(SpaceModel node) { + result.add(node); + for (final child in node.children) { + traverse(child); + } + } + + traverse(root); + return result; + } + + final duplicatedSpacesList = flattenHierarchy(duplicatedSpace); + + setState(() { + spaces.addAll(duplicatedSpacesList); + + // Duplicate the connections + for (final connection in connections) { + if (originalToDuplicate.containsKey(connection.startSpace) && + originalToDuplicate.containsKey(connection.endSpace)) { + connections.add( + Connection( + startSpace: originalToDuplicate[connection.startSpace]!, + endSpace: originalToDuplicate[connection.endSpace]!, + direction: connection.direction, + ), + ); + } + } + }); + } } diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart index 7844381d..2b8d4aaf 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart @@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart'; @@ -409,7 +410,9 @@ class CreateSpaceDialogState extends State { _showTagCreateDialog( context, enteredName, + widget.isEdit, widget.products, + subspaces, ); // Edit action }) @@ -420,7 +423,12 @@ class CreateSpaceDialogState extends State { : TextButton( onPressed: () { _showTagCreateDialog( - context, enteredName, widget.products); + context, + enteredName, + widget.isEdit, + widget.products, + subspaces, + ); }, style: TextButton.styleFrom( padding: EdgeInsets.zero, @@ -558,85 +566,57 @@ class CreateSpaceDialogState extends State { ); } - void _showTagCreateDialog( - BuildContext context, String name, List? products) { - showDialog( - context: context, - builder: (BuildContext context) { - return AddDeviceTypeWidget( - spaceName: name, - products: products, - subspaces: subspaces, - spaceTags: tags, - allTags: [], - initialSelectedProducts: - createInitialSelectedProducts(tags, subspaces), - onSave: (selectedSpaceTags, selectedSubspaces) { - setState(() { - tags = selectedSpaceTags; - selectedSpaceModel = null; + void _showTagCreateDialog(BuildContext context, String name, bool isEdit, + List? products, List? subspaces) { + isEdit + ? showDialog( + context: context, + builder: (BuildContext context) { + return AssignTagDialog( + title: 'Edit Device', + addedProducts: TagHelper.createInitialSelectedProductsForTags( + tags, subspaces), + spaceName: name, + products: products, + subspaces: subspaces, + allTags: [], + onSave: (selectedSpaceTags, selectedSubspaces) {}, + ); + }, + ) + : showDialog( + context: context, + builder: (BuildContext context) { + return AddDeviceTypeWidget( + spaceName: name, + products: products, + subspaces: subspaces, + spaceTags: tags, + allTags: [], + initialSelectedProducts: + TagHelper.createInitialSelectedProductsForTags( + tags, subspaces), + onSave: (selectedSpaceTags, selectedSubspaces) { + setState(() { + tags = selectedSpaceTags; + selectedSpaceModel = null; - if (selectedSubspaces != null) { - if (subspaces != null) { - for (final subspace in subspaces!) { - for (final selectedSubspace in selectedSubspaces) { - if (subspace.subspaceName == - selectedSubspace.subspaceName) { - subspace.tags = selectedSubspace.tags; + if (selectedSubspaces != null) { + if (subspaces != null) { + for (final subspace in subspaces!) { + for (final selectedSubspace in selectedSubspaces) { + if (subspace.subspaceName == + selectedSubspace.subspaceName) { + subspace.tags = selectedSubspace.tags; + } + } + } } } - } - } - } - }); - }, - ); - }, - ); - } - - List createInitialSelectedProducts( - List? tags, List? subspaces) { - final Map productCounts = {}; - - if (tags != null) { - for (var tag in tags) { - if (tag.product != null) { - productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1; - } - } - } - - if (subspaces != null) { - for (var subspace in subspaces) { - if (subspace.tags != null) { - for (var tag in subspace.tags!) { - if (tag.product != null) { - productCounts[tag.product!] = - (productCounts[tag.product!] ?? 0) + 1; - } - } - } - } - } - - return productCounts.entries - .map((entry) => SelectedProduct( - productId: entry.key.uuid, - count: entry.value, - productName: entry.key.name ?? 'Unnamed', - product: entry.key, - )) - .toList(); - } - - Map _groupTags(List tags) { - final Map groupedTags = {}; - for (var tag in tags) { - if (tag.product != null) { - groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1; - } - } - return groupedTags; + }); + }, + ); + }, + ); } } diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/duplicate_process_dialog.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/duplicate_process_dialog.dart new file mode 100644 index 00000000..1f719b1a --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/duplicate_process_dialog.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DuplicateProcessDialog extends StatefulWidget { + final VoidCallback onDuplicate; + + const DuplicateProcessDialog({required this.onDuplicate, Key? key}) + : super(key: key); + + @override + _DuplicateProcessDialogState createState() => _DuplicateProcessDialogState(); +} + +class _DuplicateProcessDialogState extends State { + bool isDuplicating = true; + + @override + void initState() { + super.initState(); + _startDuplication(); + } + + void _startDuplication() async { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onDuplicate(); + }); + await Future.delayed(const Duration(seconds: 2)); + setState(() { + isDuplicating = false; + }); + + await Future.delayed(const Duration(seconds: 2)); + if (mounted) { + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: SizedBox( + width: screenWidth * 0.4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isDuplicating) ...[ + const CircularProgressIndicator( + color: ColorsManager.vividBlue, + ), + const SizedBox(height: 15), + Text( + "Duplicating in progress", + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: ColorsManager.primaryColor), + textAlign: TextAlign.center, + ), + ] else ...[ + const Icon( + Icons.check_circle, + color: ColorsManager.vividBlue, + size: 50, + ), + const SizedBox(height: 15), + Text( + "Duplicating successful", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: ColorsManager.primaryColor), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart index 4a85348f..2d9222a6 100644 --- a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart +++ b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart @@ -46,9 +46,9 @@ class AssignTagBloc extends Bloc { } emit(AssignTagLoaded( - tags: allTags, - isSaveEnabled: _validateTags(allTags), - )); + tags: allTags, + isSaveEnabled: _validateTags(allTags), + errorMessage: '')); }); on((event, emit) { @@ -117,12 +117,10 @@ class AssignTagBloc extends Bloc { } bool _validateTags(List tags) { - if (tags.isEmpty) { - return false; - } final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet(); final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); - return uniqueTags.length == tags.length && !hasEmptyTag; + final isValid = uniqueTags.length == tags.length && !hasEmptyTag; + return isValid; } String? _getValidationError(List tags) { diff --git a/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart b/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart index 959c83df..7ec19a45 100644 --- a/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart +++ b/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/dialog_dropdown.dart'; +import 'package:syncrow_web/common/dialog_textfield_dropdown.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; @@ -79,6 +82,7 @@ class AssignTagDialog extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium)), DataColumn( + numeric: false, label: Text('Tag', style: Theme.of(context).textTheme.bodyMedium)), @@ -109,10 +113,11 @@ class AssignTagDialog extends StatelessWidget { : List.generate(state.tags.length, (index) { final tag = state.tags[index]; final controller = controllers[index]; - + final availableTags = getAvailableTags( + allTags ?? [], state.tags, tag); return DataRow( cells: [ - DataCell(Text(index.toString())), + DataCell(Text((index + 1).toString())), DataCell( Row( mainAxisAlignment: @@ -123,147 +128,80 @@ class AssignTagDialog extends StatelessWidget { tag.product?.name ?? 'Unknown', overflow: TextOverflow.ellipsis, )), - IconButton( - icon: const Icon(Icons.close, - color: ColorsManager.warningRed, - size: 16), - onPressed: () { - context.read().add( - DeleteTag( - tagToDelete: tag, - tags: state.tags)); - }, - tooltip: 'Delete Tag', - ) - ], - ), - ), - DataCell( - Row( - children: [ - Expanded( - child: TextFormField( - controller: controller, - onChanged: (value) { - context - .read() - .add(UpdateTagEvent( - index: index, - tag: value.trim(), - )); - }, - decoration: const InputDecoration( - hintText: 'Enter Tag', - border: InputBorder.none, - ), - style: const TextStyle( - fontSize: 14, - color: ColorsManager.blackColor, + const SizedBox(width: 10), + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager + .lightGrayColor, + width: 1.0, ), ), - ), - SizedBox( - width: MediaQuery.of(context) - .size - .width * - 0.15, - child: PopupMenuButton( - color: ColorsManager.whiteColors, + child: IconButton( icon: const Icon( - Icons.arrow_drop_down, - color: - ColorsManager.blackColor), - onSelected: (value) { - controller.text = value; + Icons.close, + color: ColorsManager + .lightGreyColor, + size: 16, + ), + onPressed: () { context .read() - .add(UpdateTagEvent( - index: index, - tag: value, - )); - }, - itemBuilder: (context) { - return (allTags ?? []) - .where((tagValue) => !state - .tags - .map((e) => e.tag) - .contains(tagValue)) - .map((tagValue) { - return PopupMenuItem( - textStyle: const TextStyle( - color: ColorsManager - .textPrimaryColor), - value: tagValue, - child: ConstrainedBox( - constraints: - BoxConstraints( - minWidth: MediaQuery.of( - context) - .size - .width * - 0.15, - maxWidth: MediaQuery.of( - context) - .size - .width * - 0.15, - ), - child: Text( - tagValue, - overflow: TextOverflow - .ellipsis, - ), - )); - }).toList(); + .add(DeleteTag( + tagToDelete: tag, + tags: state.tags)); }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: + const BoxConstraints(), ), ), ], ), ), DataCell( - DropdownButtonHideUnderline( - child: DropdownButton( - value: tag.location ?? 'Main', - dropdownColor: ColorsManager - .whiteColors, // Dropdown background - style: const TextStyle( - color: Colors - .black), // Style for selected text - items: [ - const DropdownMenuItem( - value: 'Main Space', - child: Text( - 'Main Space', - style: TextStyle( - color: ColorsManager - .textPrimaryColor), - ), - ), - ...locations.map((location) { - return DropdownMenuItem( - value: location, - child: Text( - location, - style: const TextStyle( - color: ColorsManager - .textPrimaryColor), - ), - ); - }).toList(), - ], - onChanged: (value) { - if (value != null) { + Container( + alignment: Alignment + .centerLeft, // Align cell content to the left + child: SizedBox( + width: double + .infinity, // Ensure full width for dropdown + child: DialogTextfieldDropdown( + items: availableTags, + initialValue: tag.tag, + onSelected: (value) { + controller.text = value; + context + .read() + .add(UpdateTagEvent( + index: index, + tag: value.trim(), + )); + }, + ), + ), + ), + ), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: locations, + selectedValue: + tag.location ?? 'Main Space', + onSelected: (value) { context .read() .add(UpdateLocation( index: index, location: value, )); - } - }, - ), - ), + }, + )), ), ], ); @@ -284,11 +222,33 @@ class AssignTagDialog extends StatelessWidget { children: [ const SizedBox(width: 10), Expanded( - child: CancelButton( - label: 'Cancel', - onPressed: () async { - Navigator.of(context).pop(); - }, + child: Builder( + builder: (buttonContext) => CancelButton( + label: 'Add New Device', + onPressed: () async { + final updatedTags = List.from(state.tags); + final result = processTags(updatedTags, subspaces); + + final processedTags = + result['updatedTags'] as List; + final processedSubspaces = result['subspaces']; + + Navigator.of(context).pop(); + + await showDialog( + barrierDismissible: false, + context: context, + builder: (dialogContext) => AddDeviceTypeWidget( + products: products, + subspaces: processedSubspaces, + initialSelectedProducts: addedProducts, + allTags: allTags, + spaceName: spaceName, + spaceTags: processedTags, + ), + ); + }, + ), ), ), const SizedBox(width: 10), @@ -302,22 +262,16 @@ class AssignTagDialog extends StatelessWidget { onPressed: state.isSaveEnabled ? () async { Navigator.of(context).pop(); - final assignedTags = {}; - for (var tag in state.tags) { - if (tag.location == null || - subspaces == null) { - continue; - } - for (var subspace in subspaces!) { - if (tag.location == subspace.subspaceName) { - subspace.tags ??= []; - subspace.tags!.add(tag); - assignedTags.add(tag); - break; - } - } - } - onSave!(state.tags,subspaces); + final updatedTags = List.from(state.tags); + final result = + processTags(updatedTags, subspaces); + + final processedTags = + result['updatedTags'] as List; + final processedSubspaces = + result['subspaces'] as List; + + onSave!(processedTags, processedSubspaces); } : null, child: const Text('Save'), @@ -337,4 +291,110 @@ class AssignTagDialog extends StatelessWidget { ), ); } + + List getAvailableTags( + List allTags, List currentTags, Tag currentTag) { + return allTags + .where((tagValue) => !currentTags + .where((e) => e != currentTag) // Exclude the current row + .map((e) => e.tag) + .contains(tagValue)) + .toList(); + } + + Map processTags( + List updatedTags, List? subspaces) { + final modifiedTags = List.from(updatedTags); + final modifiedSubspaces = List.from(subspaces ?? []); + + for (var tag in modifiedTags.toList()) { + if (modifiedSubspaces.isEmpty) continue; + + final prevIndice = checkTagExistInSubspace(tag, modifiedSubspaces); + + if ((tag.location == 'Main Space' || tag.location == null) && + (prevIndice == null || + modifiedSubspaces[prevIndice].subspaceName == 'Main Space')) { + continue; + } + + if ((tag.location == 'Main Space' || tag.location == null) && + prevIndice != null) { + modifiedSubspaces[prevIndice] + .tags + ?.removeWhere((t) => t.internalId == tag.internalId); + continue; + } + + if ((tag.location != 'Main Space' && tag.location != null) && + prevIndice == null) { + final newIndex = modifiedSubspaces + .indexWhere((subspace) => subspace.subspaceName == tag.location); + if (newIndex != -1) { + if (modifiedSubspaces[newIndex] + .tags + ?.any((t) => t.internalId == tag.internalId) != + true) { + tag.location = modifiedSubspaces[newIndex].subspaceName; + modifiedSubspaces[newIndex].tags?.add(tag); + } + } + modifiedTags.removeWhere((t) => t.internalId == tag.internalId); + continue; + } + + if ((tag.location != 'Main Space' && tag.location != null) && + tag.location != modifiedSubspaces[prevIndice!].subspaceName) { + modifiedSubspaces[prevIndice] + .tags + ?.removeWhere((t) => t.internalId == tag.internalId); + final newIndex = modifiedSubspaces + .indexWhere((subspace) => subspace.subspaceName == tag.location); + if (newIndex != -1) { + if (modifiedSubspaces[newIndex] + .tags + ?.any((t) => t.internalId == tag.internalId) != + true) { + tag.location = modifiedSubspaces[newIndex].subspaceName; + modifiedSubspaces[newIndex].tags?.add(tag); + } + } + + modifiedTags.removeWhere((t) => t.internalId == tag.internalId); + continue; + } + + if ((tag.location != 'Main Space' && tag.location != null) && + tag.location == modifiedSubspaces[prevIndice!].subspaceName) { + modifiedTags.removeWhere((t) => t.internalId == tag.internalId); + continue; + } + + if ((tag.location == 'Main Space' || tag.location == null) && + prevIndice != null) { + modifiedSubspaces[prevIndice] + .tags + ?.removeWhere((t) => t.internalId == tag.internalId); + } + } + + return { + 'updatedTags': modifiedTags, + 'subspaces': modifiedSubspaces, + }; + } + + int? checkTagExistInSubspace(Tag tag, List? subspaces) { + if (subspaces == null) return null; + for (int i = 0; i < subspaces.length; i++) { + final subspace = subspaces[i]; + if (subspace.tags == null) continue; + for (var t in subspace.tags!) { + if (tag.internalId == t.internalId) { + return i; + } + } + } + return null; + } } diff --git a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart index 8cff1b35..8f9b51d7 100644 --- a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart +++ b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart @@ -99,7 +99,6 @@ class AssignTagModelsDialog extends StatelessWidget { .bodyMedium)), DataColumn( numeric: false, - headingRowAlignment: MainAxisAlignment.start, label: Text('Tag', style: Theme.of(context) .textTheme @@ -462,4 +461,5 @@ class AssignTagModelsDialog extends StatelessWidget { 'subspaces': modifiedSubspaces, }; } + } diff --git a/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart index 5426f8f0..1a1884e2 100644 --- a/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart @@ -6,7 +6,7 @@ import 'subspace_event.dart'; import 'subspace_state.dart'; class SubSpaceBloc extends Bloc { - SubSpaceBloc() : super(SubSpaceState([], [], '',{})) { + SubSpaceBloc() : super(SubSpaceState([], [], '', {})) { on((event, emit) { final existingNames = state.subSpaces.map((e) => e.subspaceName).toSet(); @@ -26,13 +26,22 @@ class SubSpaceBloc extends Bloc { final updatedSubSpaces = List.from(state.subSpaces) ..add(event.subSpace); - emit(SubSpaceState( - updatedSubSpaces, - state.updatedSubSpaceModels, - '', - state.duplicates, + if (state.duplicates.isNotEmpty) { + emit(SubSpaceState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '*Duplicated sub-space name', + state.duplicates, + )); + } else { + emit(SubSpaceState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '', + state.duplicates, // Clear error message - )); + )); + } } }); @@ -45,6 +54,13 @@ class SubSpaceBloc extends Bloc { state.updatedSubSpaceModels, ); + if (event.subSpace.uuid?.isNotEmpty ?? false) { + updatedSubspaceModels.add(UpdateSubspaceModel( + action: Action.delete, + uuid: event.subSpace.uuid!, + )); + } + final nameOccurrences = {}; for (final subSpace in updatedSubSpaces) { final lowerName = subSpace.subspaceName.toLowerCase(); @@ -55,19 +71,17 @@ class SubSpaceBloc extends Bloc { .where((entry) => entry.value > 1) .map((entry) => entry.key) .toSet(); - if (event.subSpace.uuid?.isNotEmpty ?? false) { - updatedSubspaceModels.add(UpdateSubspaceModel( - action: Action.delete, - uuid: event.subSpace.uuid!, - )); - } + final errorMessage = + updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : ''; - emit(SubSpaceState(updatedSubSpaces, updatedSubspaceModels, '', - updatedDuplicates // Clear error message - )); + emit(SubSpaceState( + updatedSubSpaces, + updatedSubspaceModels, + errorMessage, + updatedDuplicates, + )); }); - // Handle UpdateSubSpace Event on((event, emit) { final updatedSubSpaces = state.subSpaces.map((subSpace) { diff --git a/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart index 1e8d0ddc..a331aed2 100644 --- a/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart +++ b/lib/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart @@ -25,22 +25,30 @@ class SubSpaceModelBloc extends Bloc { updatedDuplicates, )); } else { - // Add subspace if no duplicate exists final updatedSubSpaces = List.from(state.subSpaces) ..add(event.subSpace); - emit(SubSpaceModelState( - updatedSubSpaces, - state.updatedSubSpaceModels, - '', - state.duplicates, -// Clear error message - )); + if (state.duplicates.isNotEmpty) { + emit(SubSpaceModelState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '*Duplicated sub-space name', + state.duplicates, + )); + } else { + emit(SubSpaceModelState( + updatedSubSpaces, + state.updatedSubSpaceModels, + '', + state.duplicates, + )); + } } }); // Handle RemoveSubSpaceModel Event + on((event, emit) { final updatedSubSpaces = List.from(state.subSpaces) ..remove(event.subSpace); @@ -48,16 +56,6 @@ class SubSpaceModelBloc extends Bloc { final updatedSubspaceModels = List.from( state.updatedSubSpaceModels, ); - final nameOccurrences = {}; - for (final subSpace in updatedSubSpaces) { - final lowerName = subSpace.subspaceName.toLowerCase(); - nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1; - } - - final updatedDuplicates = nameOccurrences.entries - .where((entry) => entry.value > 1) - .map((entry) => entry.key) - .toSet(); if (event.subSpace.uuid?.isNotEmpty ?? false) { updatedSubspaceModels.add(UpdateSubspaceTemplateModel( @@ -66,12 +64,28 @@ class SubSpaceModelBloc extends Bloc { )); } + // Count occurrences of sub-space names to identify duplicates + final nameOccurrences = {}; + for (final subSpace in updatedSubSpaces) { + final lowerName = subSpace.subspaceName.toLowerCase(); + nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1; + } + + // Identify duplicate names + final updatedDuplicates = nameOccurrences.entries + .where((entry) => entry.value > 1) + .map((entry) => entry.key) + .toSet(); + + // Determine the error message + final errorMessage = + updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : ''; + emit(SubSpaceModelState( updatedSubSpaces, updatedSubspaceModels, - '', + errorMessage, updatedDuplicates, -// Clear error message )); }); diff --git a/lib/pages/spaces_management/helper/tag_helper.dart b/lib/pages/spaces_management/helper/tag_helper.dart index 4fa86b88..041f005f 100644 --- a/lib/pages/spaces_management/helper/tag_helper.dart +++ b/lib/pages/spaces_management/helper/tag_helper.dart @@ -1,6 +1,8 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; @@ -30,7 +32,36 @@ class TagHelper { } } } - + + return initialTags; + } + + static List generateInitialForTags({ + List? spaceTags, + List? subspaces, + }) { + final List initialTags = []; + + if (spaceTags != null) { + initialTags.addAll(spaceTags); + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + initialTags.addAll( + subspace.tags!.map( + (tag) => tag.copyWith( + location: subspace.subspaceName, + internalId: tag.internalId, + tag: tag.tag, + ), + ), + ); + } + } + } + return initialTags; } @@ -79,4 +110,39 @@ class TagHelper { )) .toList(); } + + static List createInitialSelectedProductsForTags( + List? tags, List? subspaces) { + final Map productCounts = {}; + + if (tags != null) { + for (var tag in tags) { + if (tag.product != null) { + productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1; + } + } + } + + if (subspaces != null) { + for (var subspace in subspaces) { + if (subspace.tags != null) { + for (var tag in subspace.tags!) { + if (tag.product != null) { + productCounts[tag.product!] = + (productCounts[tag.product!] ?? 0) + 1; + } + } + } + } + } + + return productCounts.entries + .map((entry) => SelectedProduct( + productId: entry.key.uuid, + count: entry.value, + productName: entry.key.name ?? 'Unnamed', + product: entry.key, + )) + .toList(); + } } diff --git a/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart b/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart index 740ff832..36efaaa5 100644 --- a/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart +++ b/lib/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart @@ -201,106 +201,101 @@ class CreateSpaceModelBloc on((event, emit) async { try { - final prevSpaceModel = event.spaceTemplate; - final newSpaceModel = event.updatedSpaceTemplate; - String? spaceModelName; - if (prevSpaceModel.modelName != newSpaceModel.modelName) { - spaceModelName = newSpaceModel.modelName; - } - List tagUpdates = []; - final List subspaceUpdates = []; - final List? prevSubspaces = - prevSpaceModel.subspaceModels; - final List? newSubspaces = - newSpaceModel.subspaceModels; + if (event.spaceTemplate.uuid != null) { + final prevSpaceModel = + await _api.getSpaceModel(event.spaceTemplate.uuid ?? ''); - tagUpdates = processTagUpdates(prevSpaceModel.tags, newSpaceModel.tags); + final newSpaceModel = event.updatedSpaceTemplate; + String? spaceModelName; + if (prevSpaceModel?.modelName != newSpaceModel.modelName) { + spaceModelName = newSpaceModel.modelName; + } + List tagUpdates = []; + final List subspaceUpdates = []; + final List? prevSubspaces = + prevSpaceModel?.subspaceModels; + final List? newSubspaces = + newSpaceModel.subspaceModels; - if (prevSubspaces != null || newSubspaces != null) { - if (prevSubspaces != null && newSubspaces != null) { - for (var prevSubspace in prevSubspaces) { - final existsInNew = newSubspaces - .any((newTag) => newTag.uuid == prevSubspace.uuid); - if (!existsInNew) { + tagUpdates = + processTagUpdates(prevSpaceModel?.tags, newSpaceModel.tags); + + if (prevSubspaces != null || newSubspaces != null) { + if (prevSubspaces != null && newSubspaces != null) { + for (var prevSubspace in prevSubspaces) { + final existsInNew = newSubspaces + .any((subspace) => subspace.uuid == prevSubspace.uuid); + if (!existsInNew) { + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.delete, uuid: prevSubspace.uuid)); + } + } + } else if (prevSubspaces != null && newSubspaces == null) { + for (var prevSubspace in prevSubspaces) { subspaceUpdates.add(UpdateSubspaceTemplateModel( action: Action.delete, uuid: prevSubspace.uuid)); } } - } else if (prevSubspaces != null && newSubspaces == null) { - for (var prevSubspace in prevSubspaces) { - subspaceUpdates.add(UpdateSubspaceTemplateModel( - action: Action.delete, uuid: prevSubspace.uuid)); - } - } - if (newSubspaces != null) { - for (var newSubspace in newSubspaces!) { - // Tag without UUID - if ((newSubspace.uuid == null || newSubspace.uuid!.isEmpty)) { - final List tagUpdates = []; + if (newSubspaces != null) { + for (var newSubspace in newSubspaces!) { + // Tag without UUID + if ((newSubspace.uuid == null || newSubspace.uuid!.isEmpty)) { + final List tagUpdates = []; - if (newSubspace.tags != null) { - for (var tag in newSubspace.tags!) { - tagUpdates.add(TagModelUpdate( - action: Action.add, - uuid: tag.uuid == '' ? null : tag.uuid, - tag: tag.tag, - productUuid: tag.product?.uuid)); + if (newSubspace.tags != null) { + for (var tag in newSubspace.tags!) { + tagUpdates.add(TagModelUpdate( + action: Action.add, + uuid: tag.uuid == '' ? null : tag.uuid, + tag: tag.tag, + productUuid: tag.product?.uuid)); + } } + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.add, + subspaceName: newSubspace.subspaceName, + tags: tagUpdates)); + } + } + } + + if (prevSubspaces != null && newSubspaces != null) { + final newSubspaceMap = { + for (var subspace in newSubspaces!) subspace.uuid: subspace + }; + + for (var prevSubspace in prevSubspaces) { + final newSubspace = newSubspaceMap[prevSubspace.uuid]; + + if (newSubspace != null) { + final List tagSubspaceUpdates = + processTagUpdates(prevSubspace.tags, newSubspace.tags); + subspaceUpdates.add(UpdateSubspaceTemplateModel( + action: Action.update, + uuid: newSubspace.uuid, + subspaceName: newSubspace.subspaceName, + tags: tagSubspaceUpdates)); } - subspaceUpdates.add(UpdateSubspaceTemplateModel( - action: Action.add, - subspaceName: newSubspace.subspaceName, - tags: tagUpdates)); } } } - if (prevSubspaces != null && newSubspaces != null) { - final newSubspaceMap = { - for (var subspace in newSubspaces!) subspace.uuid: subspace - }; + final spaceModelBody = CreateSpaceTemplateBodyModel( + modelName: spaceModelName, + tags: tagUpdates, + subspaceModels: subspaceUpdates); - for (var prevSubspace in prevSubspaces) { - final newSubspace = newSubspaceMap[prevSubspace.uuid]; + final res = await _api.updateSpaceModel( + spaceModelBody, prevSpaceModel?.uuid ?? ''); - if (newSubspace != null) { - if(prevSubspace.tags!=null){ - for(var t in prevSubspace.tags!){ - print("old tags are ${t.tag} ${t.uuid}"); - }} - - if(newSubspace.tags!=null){ - for(var t in newSubspace.tags!){ - print("new tags are ${t.tag} ${t.uuid}"); - }} - - final List tagSubspaceUpdates = - processTagUpdates(prevSubspace.tags, newSubspace.tags); - subspaceUpdates.add(UpdateSubspaceTemplateModel( - action: Action.update, - uuid: newSubspace.uuid, - subspaceName: newSubspace.subspaceName, - tags: tagSubspaceUpdates)); - } + if (res != null) { + emit(CreateSpaceModelLoaded(newSpaceModel)); + if (event.onUpdate != null) { + event.onUpdate!(event.updatedSpaceTemplate); } } } - - final spaceModelBody = CreateSpaceTemplateBodyModel( - modelName: spaceModelName, - tags: tagUpdates, - subspaceModels: subspaceUpdates); - - final res = await _api.updateSpaceModel( - spaceModelBody, prevSpaceModel.uuid ?? ''); - - if (res != null) { - emit(CreateSpaceModelLoaded(newSpaceModel)); - if (event.onUpdate != null) { - event.onUpdate!(event.updatedSpaceTemplate); - } - } } catch (e) { emit(CreateSpaceModelError('Error creating space model')); } @@ -314,6 +309,18 @@ class CreateSpaceModelBloc final List tagUpdates = []; final processedTags = {}; + if (prevTags == null && newTags != null) { + for (var newTag in newTags) { + tagUpdates.add(TagModelUpdate( + action: Action.add, + tag: newTag.tag, + uuid: newTag.uuid, + productUuid: newTag.product?.uuid, + )); + } + return tagUpdates; + } + if (newTags != null || prevTags != null) { // Case 1: Tags deleted if (prevTags != null && newTags != null) { @@ -334,9 +341,11 @@ class CreateSpaceModelBloc // Case 2: Tags added if (newTags != null) { + final prevTagUuids = prevTags?.map((t) => t.uuid).toSet() ?? {}; + for (var newTag in newTags!) { // Tag without UUID - if ((newTag.uuid == null || newTag.uuid!.isEmpty) && + if ((newTag.uuid == null || !prevTagUuids.contains(newTag.uuid)) && !processedTags.contains(newTag.tag)) { tagUpdates.add(TagModelUpdate( action: Action.add, diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 301365ed..4d3dbb0c 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -70,5 +70,5 @@ abstract class ColorsManager { static const Color invitedOrangeText = Color(0xFFFFBF00); static const Color lightGrayBorderColor = Color(0xB2D5D5D5); //background: #F8F8F8; - + static const Color vividBlue = Color(0xFF023DFE); } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index b151890a..d5d216c5 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -399,5 +399,7 @@ class Assets { static const String ZtoAIcon = 'assets/icons/ztoa_icon.png'; static const String AtoZIcon = 'assets/icons/atoz_icon.png'; static const String link = 'assets/icons/link.svg'; + static const String duplicate = 'assets/icons/duplicate.svg'; + static const String spaceDelete = 'assets/icons/space_delete.svg'; } //user_management.svg