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/edit_space.svg b/assets/icons/edit_space.svg new file mode 100644 index 00000000..417cd5bd --- /dev/null +++ b/assets/icons/edit_space.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + 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/common/edit_chip.dart b/lib/common/edit_chip.dart new file mode 100644 index 00000000..1643b414 --- /dev/null +++ b/lib/common/edit_chip.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EditChip extends StatelessWidget { + final String label; + final VoidCallback onTap; + final Color labelColor; + final Color backgroundColor; + final Color borderColor; + final double borderRadius; + + const EditChip({ + Key? key, + this.label = 'Edit', + required this.onTap, + this.labelColor = ColorsManager.spaceColor, + this.backgroundColor = ColorsManager.whiteColors, + this.borderColor = ColorsManager.spaceColor, + this.borderRadius = 16.0, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Chip( + label: Text( + label, + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: labelColor) + ), + backgroundColor: backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius), + side: BorderSide(color: borderColor), + ), + ), + ); + } +} diff --git a/lib/pages/common/buttons/default_button.dart b/lib/pages/common/buttons/default_button.dart index 4aa748b7..ecca6138 100644 --- a/lib/pages/common/buttons/default_button.dart +++ b/lib/pages/common/buttons/default_button.dart @@ -19,12 +19,14 @@ class DefaultButton extends StatelessWidget { this.padding, this.borderColor, this.elevation, + this.borderWidth = 1.0, }); final void Function()? onPressed; final Widget child; final double? height; final bool isSecondary; final double? borderRadius; + final double borderWidth; final bool enabled; final double? padding; final bool isDone; @@ -66,13 +68,16 @@ class DefaultButton extends StatelessWidget { }), shape: WidgetStateProperty.all( RoundedRectangleBorder( - side: BorderSide(color: borderColor ?? Colors.transparent), + side: BorderSide( + color: borderColor ?? Colors.transparent, + width: borderWidth, + ), borderRadius: BorderRadius.circular(borderRadius ?? 20), ), ), fixedSize: height != null - ? WidgetStateProperty.all(Size.fromHeight(height!)) - : null, + ? WidgetStateProperty.all(Size.fromHeight(height!)) + : null, padding: WidgetStateProperty.all( EdgeInsets.all(padding ?? 10), ), diff --git a/lib/pages/device_managment/all_devices/models/device_subspace.model.dart b/lib/pages/device_managment/all_devices/models/device_subspace.model.dart new file mode 100644 index 00000000..dc2386de --- /dev/null +++ b/lib/pages/device_managment/all_devices/models/device_subspace.model.dart @@ -0,0 +1,47 @@ +class DeviceSubspace { + final String uuid; + final DateTime? createdAt; + final DateTime? updatedAt; + final String subspaceName; + final bool disabled; + + DeviceSubspace({ + required this.uuid, + this.createdAt, + this.updatedAt, + required this.subspaceName, + required this.disabled, + }); + + factory DeviceSubspace.fromJson(Map json) { + return DeviceSubspace( + uuid: json['uuid'] as String, + createdAt: json['createdAt'] != null + ? DateTime.tryParse(json['createdAt'].toString()) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.tryParse(json['updatedAt'].toString()) + : null, + subspaceName: json['subspaceName'] as String, + disabled: json['disabled'] as bool, + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + 'subspaceName': subspaceName, + 'disabled': disabled, + }; + } + + static List listFromJson(List jsonList) { + return jsonList.map((json) => DeviceSubspace.fromJson(json)).toList(); + } + + static List> listToJson(List subspaces) { + return subspaces.map((subspace) => subspace.toJson()).toList(); + } +} diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index d30afc03..7bcf1b3d 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -1,5 +1,6 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_community.model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_subspace.model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart'; @@ -47,6 +48,7 @@ class AllDevicesModel { */ DevicesModelRoom? room; + DeviceSubspace? subspace; DevicesModelUnit? unit; DeviceCommunityModel? community; String? productUuid; @@ -77,6 +79,7 @@ class AllDevicesModel { AllDevicesModel({ this.room, + this.subspace, this.unit, this.community, this.productUuid, @@ -110,6 +113,9 @@ class AllDevicesModel { room = (json['room'] != null && (json['room'] is Map)) ? DevicesModelRoom.fromJson(json['room']) : null; + subspace = (json['subspace'] != null && (json['subspace'] is Map)) + ? DeviceSubspace.fromJson(json['subspace']) + : null; unit = (json['unit'] != null && (json['unit'] is Map)) ? DevicesModelUnit.fromJson(json['unit']) : null; @@ -276,6 +282,9 @@ SOS if (room != null) { data['room'] = room!.toJson(); } + if (subspace != null) { + data['subspace'] = subspace!.toJson(); + } if (unit != null) { data['unit'] = unit!.toJson(); } @@ -318,6 +327,7 @@ SOS return other is AllDevicesModel && other.room == room && + other.subspace == subspace && other.unit == unit && other.productUuid == productUuid && other.productType == productType && @@ -348,6 +358,7 @@ SOS @override int get hashCode { return room.hashCode ^ + subspace.hashCode ^ unit.hashCode ^ productUuid.hashCode ^ productType.hashCode ^ diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index aa1153af..7304dd07 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -95,8 +95,9 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ]), TableRow( children: [ - _buildInfoRow('Space Name:', device.unit?.name ?? 'N/A'), - _buildInfoRow('Room:', device.room?.name ?? 'N/A'), + _buildInfoRow('Space Name:', + device.spaces?.firstOrNull?.spaceName ?? 'N/A'), + _buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'), ], ), TableRow( @@ -111,9 +112,13 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), _buildInfoRow( 'Battery Level:', - device.batteryLevel != null ? '${device.batteryLevel ?? 0}%' : "-", + device.batteryLevel != null + ? '${device.batteryLevel ?? 0}%' + : "-", statusColor: device.batteryLevel != null - ? (device.batteryLevel! < 20 ? ColorsManager.red : ColorsManager.green) + ? (device.batteryLevel! < 20 + ? ColorsManager.red + : ColorsManager.green) : null, ), ], 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..1930963b 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,19 @@ 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/tag_model/widgets/action_button_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; - class AddDeviceTypeWidget extends StatelessWidget { final List? products; final ValueChanged>? onProductsSelected; @@ -20,11 +22,12 @@ class AddDeviceTypeWidget extends StatelessWidget { final List? spaceTags; final List? allTags; final String spaceName; - final Function(List,List?)? onSave; - + final bool isCreate; + final Function(List, List?)? onSave; const AddDeviceTypeWidget( {super.key, + required this.isCreate, this.products, this.initialSelectedProducts, this.onProductsSelected, @@ -44,30 +47,47 @@ 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( + initialProductCounts: state.selectedProducts, + products: products, + isCreate: isCreate, + crossAxisCount: crossAxisCount), + ), + ), + ], ), - ], - ), - ), - ), + ), + ); + } + return const SizedBox(); + }), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -78,72 +98,54 @@ 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: onSave), + ); + } }, - ), - ); - } - }, - ), + child: const Text('Next'), + ); + }, + )), ], ), ], ), )); } - - List generateInitialTags({ - 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), - ), - ); - } - } - } - - return initialTags; - } } diff --git a/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart b/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart index 08ad79ac..db2d6014 100644 --- a/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart +++ b/lib/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart @@ -13,12 +13,13 @@ import 'package:syncrow_web/utils/constants/assets.dart'; class DeviceTypeTileWidget extends StatelessWidget { final ProductModel product; final List productCounts; + final bool isCreate; - const DeviceTypeTileWidget({ - super.key, - required this.product, - required this.productCounts, - }); + const DeviceTypeTileWidget( + {super.key, + required this.product, + required this.productCounts, + required this.isCreate}); @override Widget build(BuildContext context) { @@ -48,7 +49,7 @@ class DeviceTypeTileWidget extends StatelessWidget { DeviceNameWidget(name: product.name), const SizedBox(height: 4), CounterWidget( - isCreate: false, + isCreate: isCreate, initialCount: selectedProduct.count, onCountChanged: (newCount) { context.read().add( 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..4056744e 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'; @@ -9,13 +10,14 @@ class ScrollableGridViewWidget extends StatelessWidget { final List? products; final int crossAxisCount; final List? initialProductCounts; + final bool isCreate; - const ScrollableGridViewWidget({ - super.key, - required this.products, - required this.crossAxisCount, - this.initialProductCounts, - }); + const ScrollableGridViewWidget( + {super.key, + required this.products, + required this.crossAxisCount, + this.initialProductCounts, + required this.isCreate}); @override Widget build(BuildContext context) { @@ -24,8 +26,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, @@ -42,6 +47,7 @@ class ScrollableGridViewWidget extends StatelessWidget { return DeviceTypeTileWidget( product: product, + isCreate: isCreate, productCounts: initialProductCount != null ? [...productCounts, initialProductCount] : productCounts, diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart index ff584f52..1b5692c6 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart @@ -5,12 +5,15 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_mod import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.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/space_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart'; import 'package:syncrow_web/services/product_api.dart'; import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/services/space_model_mang_api.dart'; +import 'package:syncrow_web/utils/constants/action_enum.dart'; class SpaceManagementBloc extends Bloc { @@ -341,7 +344,7 @@ class SpaceManagementBloc products: _cachedProducts ?? [], selectedCommunity: selectedCommunity, selectedSpace: selectedSpace, - spaceModels: spaceModels ?? [])); + spaceModels: spaceModels)); } } catch (e) { emit(SpaceManagementError('Error updating state: $e')); @@ -428,6 +431,76 @@ class SpaceManagementBloc for (var space in orderedSpaces) { try { if (space.uuid != null && space.uuid!.isNotEmpty) { + List tagUpdates = []; + + final prevSpace = await _api.getSpace(communityUuid, space.uuid!); + final List subspaceUpdates = []; + final List? prevSubspaces = prevSpace?.subspaces; + final List? newSubspaces = space.subspaces; + + tagUpdates = processTagUpdates(prevSpace?.tags, space.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)); + } + } + + 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)); + } + } + 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)); + } + } + } + } + final response = await _api.updateSpace( communityId: communityUuid, spaceId: space.uuid!, @@ -436,6 +509,8 @@ class SpaceManagementBloc isPrivate: space.isPrivate, position: space.position, icon: space.icon, + subspaces: subspaceUpdates, + tags: tagUpdates, direction: space.incomingConnection?.direction, ); } else { @@ -535,4 +610,79 @@ class SpaceManagementBloc emit(SpaceManagementError('Error loading communities and spaces: $e')); } } + + List processTagUpdates( + List? prevTags, + List? newTags, + ) { + 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) { + for (var prevTag in prevTags) { + final existsInNew = + newTags.any((newTag) => newTag.uuid == prevTag.uuid); + if (!existsInNew) { + tagUpdates + .add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid)); + } + } + } else if (prevTags != null && newTags == null) { + for (var prevTag in prevTags) { + tagUpdates + .add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid)); + } + } + + // 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 || !prevTagUuids.contains(newTag.uuid)) && + !processedTags.contains(newTag.tag)) { + tagUpdates.add(TagModelUpdate( + action: Action.add, + tag: newTag.tag, + uuid: newTag.uuid == '' ? null : newTag.uuid, + productUuid: newTag.product?.uuid)); + processedTags.add(newTag.tag); + } + } + } + + // Case 3: Tags updated + if (prevTags != null && newTags != null) { + final newTagMap = {for (var tag in newTags) tag.uuid: tag}; + + for (var prevTag in prevTags) { + final newTag = newTagMap[prevTag.uuid]; + if (newTag != null) { + tagUpdates.add(TagModelUpdate( + action: Action.update, + uuid: newTag.uuid, + tag: newTag.tag, + )); + } else {} + } + } + } + + return tagUpdates; + } } diff --git a/lib/pages/spaces_management/all_spaces/model/base_tag.dart b/lib/pages/spaces_management/all_spaces/model/base_tag.dart new file mode 100644 index 00000000..57f223f4 --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/model/base_tag.dart @@ -0,0 +1,26 @@ +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:uuid/uuid.dart'; + +abstract class BaseTag { + String? uuid; + String? tag; + final ProductModel? product; + String internalId; + String? location; + + BaseTag({ + this.uuid, + required this.tag, + this.product, + String? internalId, + this.location, + }) : internalId = internalId ?? const Uuid().v4(); + + Map toJson(); + BaseTag copyWith({ + String? tag, + ProductModel? product, + String? location, + String? internalId, + }); +} diff --git a/lib/pages/spaces_management/all_spaces/model/space_model.dart b/lib/pages/spaces_management/all_spaces/model/space_model.dart index c8da9d9e..6ad91dad 100644 --- a/lib/pages/spaces_management/all_spaces/model/space_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/space_model.dart @@ -66,7 +66,6 @@ class SpaceModel { final instance = SpaceModel( internalId: internalId, uuid: json['uuid'] ?? '', - spaceTuyaUuid: json['spaceTuyaUuid'], name: json['spaceName'], isPrivate: json['isPrivate'] ?? false, invitationCode: json['invitationCode'], diff --git a/lib/pages/spaces_management/all_spaces/model/subspace_model.dart b/lib/pages/spaces_management/all_spaces/model/subspace_model.dart index 2c86523f..a89ec409 100644 --- a/lib/pages/spaces_management/all_spaces/model/subspace_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/subspace_model.dart @@ -1,5 +1,6 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/utils/constants/action_enum.dart'; +import 'package:uuid/uuid.dart'; import 'tag.dart'; @@ -8,19 +9,24 @@ class SubspaceModel { String subspaceName; final bool disabled; List? tags; + String internalId; SubspaceModel({ this.uuid, required this.subspaceName, required this.disabled, this.tags, - }); + String? internalId, + }) : internalId = internalId ?? const Uuid().v4(); factory SubspaceModel.fromJson(Map json) { + final String internalId = json['internalId'] ?? const Uuid().v4(); + return SubspaceModel( uuid: json['uuid'] ?? '', subspaceName: json['subspaceName'] ?? '', disabled: json['disabled'] ?? false, + internalId: internalId, tags: (json['tags'] as List?) ?.map((item) => Tag.fromJson(item)) .toList() ?? @@ -43,7 +49,7 @@ class UpdateSubspaceModel { final Action action; final String? subspaceName; final List? tags; - UpdateSubspaceModel({ + UpdateSubspaceModel({ required this.action, required this.uuid, this.subspaceName, @@ -107,4 +113,4 @@ class UpdateTag { 'product': product?.toMap(), }; } -} \ No newline at end of file +} diff --git a/lib/pages/spaces_management/all_spaces/model/tag.dart b/lib/pages/spaces_management/all_spaces/model/tag.dart index 98494f7f..34bd08bb 100644 --- a/lib/pages/spaces_management/all_spaces/model/tag.dart +++ b/lib/pages/spaces_management/all_spaces/model/tag.dart @@ -1,22 +1,23 @@ +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/space_model/models/create_space_template_body_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; import 'package:uuid/uuid.dart'; -class Tag { - String? uuid; - String? tag; - final ProductModel? product; - String internalId; - String? location; - - Tag( - {this.uuid, - required this.tag, - this.product, - String? internalId, - this.location}) - : internalId = internalId ?? const Uuid().v4(); +class Tag extends BaseTag { + Tag({ + String? uuid, + required String? tag, + ProductModel? product, + String? internalId, + String? location, + }) : super( + uuid: uuid, + tag: tag, + product: product, + internalId: internalId, + location: location, + ); factory Tag.fromJson(Map json) { final String internalId = json['internalId'] ?? const Uuid().v4(); @@ -31,15 +32,19 @@ class Tag { ); } + @override Tag copyWith({ String? tag, ProductModel? product, String? location, + String? internalId, }) { return Tag( + uuid: uuid, tag: tag ?? this.tag, product: product ?? this.product, location: location ?? this.location, + internalId: internalId ?? this.internalId, ); } @@ -60,7 +65,7 @@ extension TagModelExtensions on Tag { ..productUuid = product?.uuid; } - CreateTagBodyModel toCreateTagBodyModel() { + CreateTagBodyModel toCreateTagBodyModel() { return CreateTagBodyModel() ..tag = tag ?? '' ..productUuid = product?.uuid; 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 new file mode 100644 index 00000000..f5188c1e --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart'; +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, + 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 + Widget build(BuildContext context) { + final canShowActions = selectedSpace != null && + selectedSpace?.status != SpaceStatus.deleted && + selectedSpace?.status != SpaceStatus.parentDeleted; + + return Wrap( + alignment: WrapAlignment.end, + spacing: 10, + children: [ + if (isSave) + CommunityStructureHeaderButton( + label: "Save", + icon: const Icon(Icons.save, + size: 18, color: ColorsManager.spaceColor), + onPressed: onSave, + theme: theme, + ), + if (canShowActions) ...[ + CommunityStructureHeaderButton( + label: "Edit", + svgAsset: Assets.editSpace, + onPressed: onEdit, + theme: theme, + ), + CommunityStructureHeaderButton( + label: "Duplicate", + svgAsset: Assets.duplicate, + onPressed: onDuplicate, + theme: theme, + ), + CommunityStructureHeaderButton( + label: "Delete", + 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 new file mode 100644 index 00000000..0369f7cd --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_header_button.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CommunityStructureHeaderButton extends StatelessWidget { + const CommunityStructureHeaderButton({ + super.key, + required this.label, + this.icon, + required this.onPressed, + this.svgAsset, + required this.theme, + }); + + final String label; + final Widget? icon; + final VoidCallback onPressed; + final String? svgAsset; + final ThemeData theme; + + @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 (icon != null) icon!, + if (svgAsset != null) + SvgPicture.asset( + svgAsset!, + width: 20, + height: 20, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + label, + style: theme.textTheme.bodySmall + ?.copyWith(color: ColorsManager.blackColor, fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} 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 02d3819a..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 @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_action_button.dart'; import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.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() => @@ -141,70 +146,18 @@ class _CommunityStructureHeaderState extends State { ), ), const SizedBox(width: 8), - _buildActionButtons(theme), + CommunityStructureHeaderActionButtons( + theme: theme, + isSave: widget.isSave, + onSave: widget.onSave, + onDelete: widget.onDelete, + onDuplicate: widget.onDuplicate, + onEdit: widget.onEdit, + selectedSpace: widget.selectedSpace, + ), ], ), ], ); } - - Widget _buildActionButtons(ThemeData theme) { - return Wrap( - alignment: WrapAlignment.end, - spacing: 10, - children: [ - if (widget.isSave) - _buildButton( - label: "Save", - icon: const Icon(Icons.save, - size: 18, color: ColorsManager.spaceColor), - onPressed: widget.onSave, - theme: theme), - if(widget.selectedSpace!= null) - _buildButton( - label: "Delete", - icon: const Icon(Icons.delete, - size: 18, color: ColorsManager.warningRed), - onPressed: widget.onDelete, - theme: theme), - ], - ); - } - - Widget _buildButton( - {required String label, - required Widget icon, - required VoidCallback onPressed, - required ThemeData theme}) { - const double buttonHeight = 30; - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: 80, minHeight: buttonHeight), - child: DefaultButton( - onPressed: onPressed, - backgroundColor: ColorsManager.textFieldGreyColor, - foregroundColor: ColorsManager.blackColor, - borderRadius: 8.0, - padding: 2.0, - height: buttonHeight, - elevation: 0, - borderColor: ColorsManager.lightGrayColor, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - icon, - const SizedBox(width: 5), - Flexible( - child: Text( - label, - style: theme.textTheme.bodySmall - ?.copyWith(color: ColorsManager.blackColor), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ], - ), - ), - ); - } } 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 f569d252..3a208a0d 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: () => {_showEditSpaceDialog()}, onEditName: () { setState(() { isEditingName = !isEditingName; @@ -190,6 +195,7 @@ class _CommunityStructureAreaState extends State { screenSize, position: spaces[index].position + newPosition, + parentIndex: index, direction: direction, ); @@ -209,9 +215,6 @@ class _CommunityStructureAreaState extends State { opacity: isHighlighted ? 1.0 : 0.3, child: SpaceContainerWidget( index: index, - onDoubleTap: () { - _showEditSpaceDialog(spaces[index]); - }, onTap: () { _selectSpace(context, spaces[index]); }, @@ -292,6 +295,7 @@ class _CommunityStructureAreaState extends State { return CreateSpaceDialog( products: widget.products, spaceModels: widget.spaceModels, + allTags: _getAllTagValues(spaces), parentSpace: parentIndex != null ? spaces[parentIndex] : null, onCreateSpace: (String name, String icon, @@ -328,7 +332,6 @@ class _CommunityStructureAreaState extends State { parentSpace.addOutgoingConnection(newConnection); parentSpace.children.add(newSpace); } - spaces.add(newSpace); _updateNodePosition(newSpace, newSpace.position); }); @@ -338,39 +341,46 @@ class _CommunityStructureAreaState extends State { ); } - void _showEditSpaceDialog(SpaceModel space) { - showDialog( - context: context, - builder: (BuildContext context) { - return CreateSpaceDialog( - products: widget.products, - name: space.name, - icon: space.icon, - editSpace: space, - isEdit: true, - onCreateSpace: (String name, - String icon, - List selectedProducts, - SpaceTemplateModel? spaceModel, - List? subspaces, - List? tags) { - setState(() { - // Update the space's properties - space.name = name; - space.icon = icon; - space.spaceModel = spaceModel; - space.subspaces = subspaces; - space.tags = tags; + void _showEditSpaceDialog() { + if (widget.selectedSpace != null) { + showDialog( + context: context, + builder: (BuildContext context) { + return CreateSpaceDialog( + products: widget.products, + spaceModels: widget.spaceModels, + name: widget.selectedSpace!.name, + icon: widget.selectedSpace!.icon, + editSpace: widget.selectedSpace, + tags: widget.selectedSpace?.tags, + subspaces: widget.selectedSpace?.subspaces, + isEdit: true, + allTags: _getAllTagValues(spaces), + onCreateSpace: (String name, + String icon, + List selectedProducts, + SpaceTemplateModel? spaceModel, + List? subspaces, + List? tags) { + setState(() { + // Update the space's properties + widget.selectedSpace!.name = name; + widget.selectedSpace!.icon = icon; + widget.selectedSpace!.spaceModel = spaceModel; + widget.selectedSpace!.subspaces = subspaces; + widget.selectedSpace!.tags = tags; - if (space.status != SpaceStatus.newSpace) { - space.status = SpaceStatus.modified; // Mark as modified - } - }); - }, - key: Key(space.name), - ); - }, - ); + if (widget.selectedSpace!.status != SpaceStatus.newSpace) { + widget.selectedSpace!.status = + SpaceStatus.modified; // Mark as modified + } + }); + }, + key: Key(widget.selectedSpace!.name), + ); + }, + ); + } } void _handleHoverChanged(int index, bool isHovered) { @@ -463,7 +473,7 @@ class _CommunityStructureAreaState extends State { _markChildrenAsDeleted(space); } } - + _removeConnectionsForDeletedSpaces(); }); } @@ -545,4 +555,206 @@ 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 = 200.0; + const double verticalGap = 100.0; + + final Map nameCounters = {}; + + String _generateCopyName(String originalName) { + final baseName = originalName.replaceAll(RegExp(r'\(\d+\)$'), '').trim(); + nameCounters[baseName] = (nameCounters[baseName] ?? 0) + 1; + return "$baseName(${nameCounters[baseName]})"; + } + + SpaceModel duplicateRecursive(SpaceModel original, Offset parentPosition, + SpaceModel? duplicatedParent) { + Offset newPosition = parentPosition + Offset(horizontalGap, 0); + + while (spaces.any((s) => + (s.position - newPosition).distance < horizontalGap && + s.status != SpaceStatus.deleted)) { + newPosition += Offset(horizontalGap, 0); + } + + final duplicatedName = _generateCopyName(original.name); + + final duplicated = SpaceModel( + name: duplicatedName, + icon: original.icon, + position: newPosition, + isPrivate: original.isPrivate, + children: [], + status: SpaceStatus.newSpace, + parent: duplicatedParent, + spaceModel: original.spaceModel, + subspaces: original.subspaces, + tags: original.tags, + ); + + originalToDuplicate[original] = duplicated; + + setState(() { + spaces.add(duplicated); + _updateNodePosition(duplicated, duplicated.position); + + if (duplicatedParent != null) { + final newConnection = Connection( + startSpace: duplicatedParent, + endSpace: duplicated, + direction: "down", + ); + connections.add(newConnection); + duplicated.incomingConnection = newConnection; + duplicatedParent.addOutgoingConnection(newConnection); + } + + if (original.parent != null && duplicatedParent == null) { + final originalParent = original.parent!; + final duplicatedParent = + originalToDuplicate[originalParent] ?? originalParent; + + final parentConnection = Connection( + startSpace: duplicatedParent, + endSpace: duplicated, + direction: original.incomingConnection?.direction ?? "down", + ); + + connections.add(parentConnection); + duplicated.incomingConnection = parentConnection; + duplicatedParent.addOutgoingConnection(parentConnection); + } + }); + + final childrenWithDownDirection = original.children + .where((child) => + child.incomingConnection?.direction == "down" && + child.status != SpaceStatus.deleted) + .toList(); + + Offset childStartPosition = childrenWithDownDirection.length == 1 + ? duplicated.position + : newPosition + Offset(0, verticalGap); + + for (final child in original.children) { + final isDownDirection = + child.incomingConnection?.direction == "down" ?? false; + + if (isDownDirection && childrenWithDownDirection.length == 1) { + // Place the only "down" child vertically aligned with the parent + childStartPosition = duplicated.position + Offset(0, verticalGap); + } else if (!isDownDirection) { + // Position children with other directions horizontally + childStartPosition = duplicated.position + Offset(horizontalGap, 0); + } + + final duplicatedChild = + duplicateRecursive(child, childStartPosition, duplicated); + duplicated.children.add(duplicatedChild); + childStartPosition += Offset(0, verticalGap); + } + + return duplicated; + } + + if (space.parent == null) { + duplicateRecursive(space, space.position, null); + } else { + final duplicatedParent = + originalToDuplicate[space.parent!] ?? space.parent!; + duplicateRecursive(space, space.position, duplicatedParent); + } + } + + List _getAllTagValues(List spaces) { + final List allTags = []; + for (final space in spaces) { + if (space.tags != null) { + allTags.addAll(space.listAllTagValues()); + } + } + return allTags; + } } 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 6f3e9fcb..ada60850 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 @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/common/edit_chip.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'; @@ -9,16 +10,25 @@ 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'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/space_icon_const.dart'; class CreateSpaceDialog extends StatefulWidget { - final Function(String, String, List selectedProducts, - SpaceTemplateModel? spaceModel, List? subspaces, List? tags) onCreateSpace; + final Function( + String, + String, + List selectedProducts, + SpaceTemplateModel? spaceModel, + List? subspaces, + List? tags) onCreateSpace; final List? products; final String? name; final String? icon; @@ -29,6 +39,7 @@ class CreateSpaceDialog extends StatefulWidget { final List? spaceModels; final List? subspaces; final List? tags; + final List? allTags; const CreateSpaceDialog( {super.key, @@ -39,6 +50,7 @@ class CreateSpaceDialog extends StatefulWidget { this.icon, this.isEdit = false, this.editSpace, + this.allTags, this.selectedProducts = const [], this.spaceModels, this.subspaces, @@ -69,6 +81,8 @@ class CreateSpaceDialogState extends State { widget.selectedProducts.isNotEmpty ? widget.selectedProducts : []; isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty; + tags = widget.tags ?? []; + subspaces = widget.subspaces ?? []; } @override @@ -139,50 +153,52 @@ class CreateSpaceDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField( - controller: nameController, - onChanged: (value) { - enteredName = value.trim(); - setState(() { - isNameFieldExist = false; - isOkButtonEnabled = false; - isNameFieldInvalid = value.isEmpty; + SizedBox( + width: screenWidth * 0.25, + child: TextField( + controller: nameController, + onChanged: (value) { + enteredName = value.trim(); + setState(() { + isNameFieldExist = false; + isOkButtonEnabled = false; + isNameFieldInvalid = value.isEmpty; - if (!isNameFieldInvalid) { - if (_isNameConflict(value)) { - isNameFieldExist = true; - isOkButtonEnabled = false; - } else { - isNameFieldExist = false; - isOkButtonEnabled = true; + if (!isNameFieldInvalid) { + if (_isNameConflict(value)) { + isNameFieldExist = true; + isOkButtonEnabled = false; + } else { + isNameFieldExist = false; + isOkButtonEnabled = true; + } } - } - }); - }, - style: const TextStyle(color: Colors.black), - decoration: InputDecoration( - hintText: 'Please enter the name', - hintStyle: const TextStyle( - fontSize: 14, - color: ColorsManager.lightGrayColor, - fontWeight: FontWeight.w400, - ), - filled: true, - fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: isNameFieldInvalid || isNameFieldExist - ? ColorsManager.red - : ColorsManager.boxColor, - width: 1.5, + }); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.lightGrayColor), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: isNameFieldInvalid || isNameFieldExist + ? ColorsManager.red + : ColorsManager.boxColor, + width: 1.5, + ), ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - width: 1.5, + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), ), ), ), @@ -211,46 +227,20 @@ class CreateSpaceDialogState extends State { ), const SizedBox(height: 10), selectedSpaceModel == null - ? DefaultButton( + ? TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), onPressed: () { _showLinkSpaceModelDialog(context); }, - backgroundColor: ColorsManager.textFieldGreyColor, - foregroundColor: Colors.black, - borderColor: ColorsManager.neutralGray, - borderRadius: 16.0, - padding: 10.0, // Reduced padding for smaller size - child: Align( - alignment: Alignment.centerLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 6.0), - child: SvgPicture.asset( - Assets.link, - width: screenWidth * - 0.015, // Adjust icon size - height: screenWidth * 0.015, - ), - ), - const SizedBox(width: 3), - Flexible( - child: Text( - 'Link a space model', - overflow: TextOverflow - .ellipsis, // Prevent overflow - style: Theme.of(context) - .textTheme - .bodyMedium, - ), - ), - ], - ), + child: const ButtonContentWidget( + svgAssets: Assets.link, + label: 'Link a space model', ), ) : Container( - width: screenWidth * 0.35, + width: screenWidth * 0.25, padding: const EdgeInsets.symmetric( vertical: 10.0, horizontal: 16.0), decoration: BoxDecoration( @@ -264,8 +254,11 @@ class CreateSpaceDialogState extends State { Chip( label: Text( selectedSpaceModel?.modelName ?? '', - style: const TextStyle( - color: ColorsManager.spaceColor), + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.spaceColor), ), backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( @@ -298,25 +291,25 @@ class CreateSpaceDialogState extends State { ), ), const SizedBox(height: 25), - const Row( + Row( children: [ - Expanded( + const Expanded( child: Divider( color: ColorsManager.neutralGray, thickness: 1.0, ), ), Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Text( 'OR', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), ), ), - Expanded( + const Expanded( child: Divider( color: ColorsManager.neutralGray, thickness: 1.0, @@ -325,48 +318,23 @@ class CreateSpaceDialogState extends State { ], ), const SizedBox(height: 25), - subspaces == null - ? DefaultButton( - onPressed: () { + subspaces == null || subspaces!.isEmpty + ? TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () async { _showSubSpaceDialog(context, enteredName, [], false, widget.products, subspaces); }, - backgroundColor: ColorsManager.textFieldGreyColor, - foregroundColor: Colors.black, - borderColor: ColorsManager.neutralGray, - borderRadius: 16.0, - padding: 10.0, // Reduced padding for smaller size - child: Align( - alignment: Alignment.centerLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 6.0), - child: SvgPicture.asset( - Assets.addIcon, - width: screenWidth * - 0.015, // Adjust icon size - height: screenWidth * 0.015, - ), - ), - const SizedBox(width: 3), - Flexible( - child: Text( - 'Create sub space', - overflow: TextOverflow - .ellipsis, // Prevent overflow - style: Theme.of(context) - .textTheme - .bodyMedium, - ), - ), - ], - ), + child: const ButtonContentWidget( + icon: Icons.add, + label: 'Create Sub Space', ), ) : SizedBox( - width: screenWidth * 0.35, + width: screenWidth * 0.25, child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( @@ -382,50 +350,46 @@ class CreateSpaceDialogState extends State { runSpacing: 8.0, children: [ if (subspaces != null) - ...subspaces!.map( - (subspace) => Chip( - label: Text( - subspace.subspaceName, - style: const TextStyle( - color: ColorsManager - .spaceColor), // Text color - ), - backgroundColor: ColorsManager - .whiteColors, // Chip background color - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 16), // Rounded chip - side: const BorderSide( - color: ColorsManager - .spaceColor), // Border color - ), - ), - ), - GestureDetector( + ...subspaces!.map((subspace) { + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SubspaceNameDisplayWidget( + text: subspace.subspaceName, + validateName: (updatedName) { + bool nameExists = + subspaces!.any((s) { + bool isSameId = s.internalId == + subspace.internalId; + bool isSameName = s.subspaceName + .trim() + .toLowerCase() == + updatedName + .trim() + .toLowerCase(); + + return !isSameId && isSameName; + }); + + return !nameExists; + }, + onNameChanged: (updatedName) { + setState(() { + subspace.subspaceName = + updatedName; + }); + }, + ), + ], + ); + }), + EditChip( onTap: () async { - _showSubSpaceDialog( - context, - enteredName, - [], - false, - widget.products, - subspaces); + _showSubSpaceDialog(context, enteredName, + [], true, widget.products, subspaces); }, - child: Chip( - label: const Text( - 'Edit', - style: TextStyle( - color: ColorsManager.spaceColor), - ), - backgroundColor: - ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide( - color: ColorsManager.spaceColor), - ), - ), - ), + ) ], ), ), @@ -452,7 +416,7 @@ class CreateSpaceDialogState extends State { runSpacing: 8.0, children: [ // Combine tags from spaceModel and subspaces - ..._groupTags([ + ...TagHelper.groupTags([ ...?tags, ...?subspaces?.expand( (subspace) => subspace.tags ?? []) @@ -469,9 +433,12 @@ class CreateSpaceDialogState extends State { ), label: Text( 'x${entry.value}', // Show count - style: const TextStyle( - color: ColorsManager.spaceColor, - ), + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: ColorsManager + .spaceColor), ), backgroundColor: ColorsManager.whiteColors, @@ -484,70 +451,53 @@ class CreateSpaceDialogState extends State { ), ), ), - GestureDetector( - onTap: () async { - _showTagCreateDialog(context, enteredName, - widget.products); - // Edit action - }, - child: Chip( - label: const Text( - 'Edit', - style: TextStyle( - color: ColorsManager.spaceColor), + + EditChip(onTap: () async { + final result = await showDialog( + context: context, + builder: (context) => AssignTagDialog( + products: widget.products, + subspaces: widget.subspaces, + addedProducts: TagHelper + .createInitialSelectedProductsForTags( + tags ?? [], subspaces), + title: 'Edit Device', + initialTags: + TagHelper.generateInitialForTags( + spaceTags: tags, + subspaces: subspaces), + spaceName: widget.name ?? '', + onSave: + (updatedTags, updatedSubspaces) { + setState(() { + tags = updatedTags; + subspaces = updatedSubspaces; + }); + }, ), - backgroundColor: - ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide( - color: ColorsManager.spaceColor), - ), - ), - ), + ); + }) ], ), ), ) - : DefaultButton( + : TextButton( onPressed: () { _showTagCreateDialog( - context, enteredName, widget.products); + context, + enteredName, + widget.isEdit, + widget.products, + subspaces, + ); }, - backgroundColor: ColorsManager.textFieldGreyColor, - foregroundColor: Colors.black, - borderColor: ColorsManager.neutralGray, - borderRadius: 16.0, - padding: 10.0, // Reduced padding for smaller size - child: Align( - alignment: Alignment.centerLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 6.0), - child: SvgPicture.asset( - Assets.addIcon, - width: screenWidth * - 0.015, // Adjust icon size - height: screenWidth * 0.015, - ), - ), - const SizedBox(width: 3), - Flexible( - child: Text( - 'Add devices', - overflow: TextOverflow - .ellipsis, // Prevent overflow - style: Theme.of(context) - .textTheme - .bodyMedium, - ), - ), - ], - ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, ), - ) + child: const ButtonContentWidget( + icon: Icons.add, + label: 'Add Devices', + )) ], ), ), @@ -579,8 +529,13 @@ class CreateSpaceDialogState extends State { ? enteredName : (widget.name ?? ''); if (newName.isNotEmpty) { - widget.onCreateSpace(newName, selectedIcon, - selectedProducts, selectedSpaceModel,subspaces,tags); + widget.onCreateSpace( + newName, + selectedIcon, + selectedProducts, + selectedSpaceModel, + subspaces, + tags); Navigator.of(context).pop(); } } @@ -635,6 +590,7 @@ class CreateSpaceDialogState extends State { setState(() { selectedSpaceModel = selectedModel; subspaces = null; + tags = null; }); } }, @@ -655,7 +611,7 @@ class CreateSpaceDialogState extends State { builder: (BuildContext context) { return CreateSubSpaceDialog( spaceName: name, - dialogTitle: 'Create Sub-space', + dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space', spaceTags: spaceTags, isEdit: isEdit, products: products, @@ -672,85 +628,76 @@ 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: widget.allTags, + 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; + } + } + } } } - } - } - } - }); - }, - ); - }, - ); - } + }); + }, + ); + }, + ) + : showDialog( + context: context, + builder: (BuildContext context) { + return AddDeviceTypeWidget( + spaceName: name, + products: products, + subspaces: subspaces, + spaceTags: tags, + isCreate: true, + allTags: widget.allTags, + initialSelectedProducts: + TagHelper.createInitialSelectedProductsForTags( + tags, subspaces), + onSave: (selectedSpaceTags, selectedSubspaces) { + setState(() { + tags = selectedSpaceTags; + selectedSpaceModel = null; - 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; + 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; + } + } + } + } + } + }); + }, + ); + }, + ); } } 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/all_spaces/widgets/sidebar_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart index b6e77b10..a38743dc 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart @@ -196,8 +196,12 @@ class _SidebarWidgetState extends State { _handleExpansionChange(community.uuid, expanded); }, children: hasChildren - ? community.spaces.map((space) => _buildSpaceTile(space, community)).toList() - : null, // Render spaces within the community + ? community.spaces + .where((space) => (space.status != SpaceStatus.deleted || + space.status != SpaceStatus.parentDeleted)) + .map((space) => _buildSpaceTile(space, community)) + .toList() + : null, ); } diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart index 6e1f50c1..62d8197c 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_widget.dart @@ -20,8 +20,7 @@ class SpaceWidget extends StatelessWidget { top: position.dy, child: GestureDetector( onTap: onTap, - child: - Container( + child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: ColorsManager.whiteColors, @@ -39,11 +38,10 @@ class SpaceWidget extends StatelessWidget { children: [ const Icon(Icons.location_on, color: ColorsManager.spaceColor), const SizedBox(width: 8), - Text(name, style: const TextStyle(fontSize: 16)), + Text(name, style: Theme.of(context).textTheme.bodyMedium), ], ), ), - ), ); } 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..cb1f7b46 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'; @@ -9,6 +12,7 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart'; +import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class AssignTagDialog extends StatelessWidget { @@ -37,8 +41,11 @@ class AssignTagDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final List locations = - (subspaces ?? []).map((subspace) => subspace.subspaceName).toList(); + final List locations = (subspaces ?? []) + .map((subspace) => subspace.subspaceName) + .toList() + ..add('Main Space'); + return BlocProvider( create: (_) => AssignTagBloc() ..add(InitializeTags( @@ -79,6 +86,7 @@ class AssignTagDialog extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium)), DataColumn( + numeric: false, label: Text('Tag', style: Theme.of(context).textTheme.bodyMedium)), @@ -89,30 +97,32 @@ class AssignTagDialog extends StatelessWidget { ], rows: state.tags.isEmpty ? [ - const DataRow(cells: [ + DataRow(cells: [ DataCell( Center( - child: Text( - 'No Data Available', - style: TextStyle( - fontSize: 14, - color: ColorsManager.lightGrayColor, - ), - ), + child: Text('No Data Available', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager + .lightGrayColor, + )), ), ), - DataCell(SizedBox()), - DataCell(SizedBox()), - DataCell(SizedBox()), + const DataCell(SizedBox()), + const DataCell(SizedBox()), + const DataCell(SizedBox()), ]) ] : 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 +133,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, )); - } - }, - ), - ), + }, + )), ), ], ); @@ -271,10 +214,11 @@ class AssignTagDialog extends StatelessWidget { ), ), if (state.errorMessage != null) - Text( - state.errorMessage!, - style: const TextStyle(color: ColorsManager.warningRed), - ), + Text(state.errorMessage!, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.warningRed)), ], ), ), @@ -284,11 +228,35 @@ 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( + context: context, + builder: (context) => AddDeviceTypeWidget( + products: products, + subspaces: processedSubspaces, + initialSelectedProducts: TagHelper + .createInitialSelectedProductsForTags( + processedTags, processedSubspaces), + spaceName: spaceName, + spaceTags: processedTags, + isCreate: false, + onSave: onSave, + ), + ); + }, + ), ), ), const SizedBox(width: 10), @@ -301,23 +269,16 @@ class AssignTagDialog extends StatelessWidget { foregroundColor: ColorsManager.whiteColors, onPressed: state.isSaveEnabled ? () async { + final updatedTags = List.from(state.tags); + final result = + processTags(updatedTags, subspaces); + + final processedTags = + result['updatedTags'] as List; + final processedSubspaces = + result['subspaces'] as List; + onSave?.call(processedTags, processedSubspaces); 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); } : null, child: const Text('Save'), @@ -337,4 +298,118 @@ 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 ?? []); + + if (subspaces != null) { + for (var subspace in subspaces) { + subspace.tags?.removeWhere( + (tag) => !modifiedTags + .any((updatedTag) => updatedTag.internalId == tag.internalId), + ); + } + } + 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 61dcd0be..9696723a 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 @@ -15,6 +15,7 @@ import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; class AssignTagModelsDialog extends StatelessWidget { final List? products; @@ -29,6 +30,7 @@ class AssignTagModelsDialog extends StatelessWidget { final String title; final BuildContext? pageContext; final List? otherSpaceModels; + final List? allSpaceModels; const AssignTagModelsDialog( {Key? key, @@ -42,7 +44,8 @@ class AssignTagModelsDialog extends StatelessWidget { required this.title, this.pageContext, this.otherSpaceModels, - this.spaceModel}) + this.spaceModel, + this.allSpaceModels}) : super(key: key); @override @@ -109,22 +112,22 @@ class AssignTagModelsDialog extends StatelessWidget { ], rows: state.tags.isEmpty ? [ - const DataRow(cells: [ + DataRow(cells: [ DataCell( Center( - child: Text( - 'No Data Available', - style: TextStyle( - fontSize: 14, - color: - ColorsManager.lightGrayColor, - ), - ), + child: Text('No Devices Available', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager + .lightGrayColor, + )), ), ), - DataCell(SizedBox()), - DataCell(SizedBox()), - DataCell(SizedBox()), + const DataCell(SizedBox()), + const DataCell(SizedBox()), + const DataCell(SizedBox()), ]) ] : List.generate(state.tags.length, (index) { @@ -211,8 +214,8 @@ class AssignTagModelsDialog extends StatelessWidget { width: double.infinity, child: DialogDropdown( items: locations, - selectedValue: - tag.location ?? 'Main Space', + selectedValue: tag.location ?? + 'Main Space', onSelected: (value) { context .read< @@ -230,11 +233,11 @@ class AssignTagModelsDialog extends StatelessWidget { ), ), if (state.errorMessage != null) - Text( - state.errorMessage!, - style: const TextStyle( - color: ColorsManager.warningRed), - ), + Text(state.errorMessage!, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.warningRed)), ], ), ), @@ -248,33 +251,14 @@ class AssignTagModelsDialog extends StatelessWidget { builder: (buttonContext) => CancelButton( label: 'Add New Device', onPressed: () async { - for (var tag in state.tags) { - if (tag.location == null || - subspaces == null) { - continue; - } + final updatedTags = + List.from(state.tags); + final result = + processTags(updatedTags, subspaces); - final previousTagSubspace = - checkTagExistInSubspace( - tag, subspaces ?? []); - - if (tag.location == 'Main Space') { - removeTagFromSubspace( - tag, previousTagSubspace); - } else if (tag.location != - previousTagSubspace?.subspaceName) { - removeTagFromSubspace( - tag, previousTagSubspace); - moveToNewSubspace(tag, subspaces ?? []); - state.tags.removeWhere( - (t) => t.internalId == tag.internalId); - } else { - updateTagInSubspace( - tag, previousTagSubspace); - state.tags.removeWhere( - (t) => t.internalId == tag.internalId); - } - } + final processedTags = + result['updatedTags'] as List; + final processedSubspaces = result['subspaces']; if (context.mounted) { Navigator.of(context).pop(); @@ -284,22 +268,25 @@ class AssignTagModelsDialog extends StatelessWidget { builder: (dialogContext) => AddDeviceTypeModelWidget( products: products, - subspaces: subspaces, + subspaces: processedSubspaces, isCreate: false, - initialSelectedProducts: - addedProducts, + initialSelectedProducts: TagHelper + .createInitialSelectedProducts( + processedTags, + processedSubspaces), allTags: allTags, spaceName: spaceName, otherSpaceModels: otherSpaceModels, - spaceTagModels: state.tags, + spaceTagModels: processedTags, pageContext: pageContext, spaceModel: SpaceTemplateModel( modelName: spaceName, - tags: state.tags, + tags: updatedTags, uuid: spaceModel?.uuid, internalId: spaceModel?.internalId, - subspaceModels: subspaces)), + subspaceModels: + processedSubspaces)), ); } }, @@ -316,33 +303,17 @@ class AssignTagModelsDialog extends StatelessWidget { foregroundColor: ColorsManager.whiteColors, onPressed: state.isSaveEnabled ? () async { - for (var tag in state.tags) { - if (tag.location == null || - subspaces == null) { - continue; - } + final updatedTags = + List.from(state.tags); + final result = + processTags(updatedTags, subspaces); - final previousTagSubspace = - checkTagExistInSubspace( - tag, subspaces ?? []); + final processedTags = + result['updatedTags'] as List; + final processedSubspaces = + result['subspaces'] + as List; - if (tag.location == 'Main Space') { - removeTagFromSubspace( - tag, previousTagSubspace); - } else if (tag.location != - previousTagSubspace?.subspaceName) { - removeTagFromSubspace( - tag, previousTagSubspace); - moveToNewSubspace(tag, subspaces ?? []); - state.tags.removeWhere((t) => - t.internalId == tag.internalId); - } else { - updateTagInSubspace( - tag, previousTagSubspace); - state.tags.removeWhere((t) => - t.internalId == tag.internalId); - } - } Navigator.of(context) .popUntil((route) => route.isFirst); @@ -351,16 +322,18 @@ class AssignTagModelsDialog extends StatelessWidget { builder: (BuildContext dialogContext) { return CreateSpaceModelDialog( products: products, + allSpaceModels: allSpaceModels, allTags: allTags, pageContext: pageContext, otherSpaceModels: otherSpaceModels, spaceModel: SpaceTemplateModel( modelName: spaceName, - tags: state.tags, + tags: processedTags, uuid: spaceModel?.uuid, internalId: spaceModel?.internalId, - subspaceModels: subspaces), + subspaceModels: + processedSubspaces), ); }, ); @@ -394,39 +367,109 @@ class AssignTagModelsDialog extends StatelessWidget { .toList(); } - void removeTagFromSubspace(TagModel tag, SubspaceTemplateModel? subspace) { - subspace?.tags?.removeWhere((t) => t.internalId == tag.internalId); - } - - SubspaceTemplateModel? checkTagExistInSubspace( + int? checkTagExistInSubspace( TagModel tag, List? subspaces) { if (subspaces == null) return null; - for (var subspace in subspaces) { - if (subspace.tags == 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 subspace; + if (tag.internalId == t.internalId) { + return i; + } } } return null; } - void moveToNewSubspace(TagModel tag, List subspaces) { - final targetSubspace = subspaces - .firstWhere((subspace) => subspace.subspaceName == tag.location); + Map processTags( + List updatedTags, List? subspaces) { + final modifiedTags = List.from(updatedTags); + final modifiedSubspaces = List.from(subspaces ?? []); - targetSubspace.tags ??= []; - if (targetSubspace.tags?.any((t) => t.internalId == tag.internalId) != - true) { - targetSubspace.tags?.add(tag); + if (subspaces != null) { + for (var subspace in subspaces) { + subspace.tags?.removeWhere( + (tag) => !modifiedTags + .any((updatedTag) => updatedTag.internalId == tag.internalId), + ); + } } - } - void updateTagInSubspace(TagModel tag, SubspaceTemplateModel? subspace) { - final currentTag = subspace?.tags?.firstWhere( - (t) => t.internalId == tag.internalId, - ); - if (currentTag != null) { - currentTag.tag = tag.tag; + 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, + }; } } diff --git a/lib/pages/spaces_management/create_community/view/create_community_dialog.dart b/lib/pages/spaces_management/create_community/view/create_community_dialog.dart index 1a5460d1..13e676b5 100644 --- a/lib/pages/spaces_management/create_community/view/create_community_dialog.dart +++ b/lib/pages/spaces_management/create_community/view/create_community_dialog.dart @@ -77,9 +77,7 @@ class CreateCommunityDialog extends StatelessWidget { .read() .add(ValidateCommunityNameEvent(value)); }, - style: const TextStyle( - color: ColorsManager.blackColor, - ), + style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( hintText: 'Please enter the community name', filled: true, 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 6a072e4a..b334a301 100644 --- a/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart @@ -6,26 +6,42 @@ 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(); + final existingNames = state.subSpaces.map((e) => e.subspaceName).toSet(); if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) { - emit(SubSpaceState( - state.subSpaces, - state.updatedSubSpaceModels, - 'Subspace name already exists.', - )); - } else { + final updatedDuplicates = Set.from(state.duplicates) + ..add(event.subSpace.subspaceName.toLowerCase()); final updatedSubSpaces = List.from(state.subSpaces) ..add(event.subSpace); - emit(SubSpaceState( updatedSubSpaces, state.updatedSubSpaceModels, - '', + '*Duplicated sub-space name', + updatedDuplicates, )); + } else { + // Add subspace if no duplicate exists + final updatedSubSpaces = List.from(state.subSpaces) + ..add(event.subSpace); + + 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,13 +61,50 @@ class SubSpaceBloc extends Bloc { )); } + 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(); + final errorMessage = + updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : ''; + emit(SubSpaceState( updatedSubSpaces, updatedSubspaceModels, - '', // Clear error message + errorMessage, + updatedDuplicates, )); }); - // Handle UpdateSubSpace Event + on((event, emit) { + final updatedSubSpaces = state.subSpaces.map((subSpace) { + if (subSpace.uuid == event.updatedSubSpace.uuid) { + return event.updatedSubSpace; + } + return subSpace; + }).toList(); + + final updatedSubspaceModels = List.from( + state.updatedSubSpaceModels, + ); + + updatedSubspaceModels.add(UpdateSubspaceModel( + action: Action.update, + uuid: event.updatedSubSpace.uuid!, + )); + + emit(SubSpaceState( + updatedSubSpaces, + updatedSubspaceModels, + '', + state.duplicates, + )); + }); } } diff --git a/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart b/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart index d1374ea1..9521ff2b 100644 --- a/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_state.dart @@ -4,23 +4,26 @@ class SubSpaceState { final List subSpaces; final List updatedSubSpaceModels; final String errorMessage; + final Set duplicates; SubSpaceState( this.subSpaces, this.updatedSubSpaceModels, this.errorMessage, + this.duplicates, ); - SubSpaceState copyWith({ List? subSpaces, List? updatedSubSpaceModels, String? errorMessage, + Set? duplicates, }) { return SubSpaceState( subSpaces ?? this.subSpaces, updatedSubSpaceModels ?? this.updatedSubSpaceModels, errorMessage ?? this.errorMessage, + duplicates ?? this.duplicates, ); } } diff --git a/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart b/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart index 6fd0b936..0a2a01e5 100644 --- a/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart +++ b/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart @@ -81,104 +81,136 @@ class CreateSubSpaceDialog extends StatelessWidget { spacing: 8.0, runSpacing: 8.0, children: [ - ...state.subSpaces.map( - (subSpace) => Chip( - label: Text( - subSpace.subspaceName, - style: const TextStyle( - color: ColorsManager.spaceColor), - ), - backgroundColor: ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: ColorsManager.transparentColor, - width: 0, - ), - ), - deleteIcon: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1.5, + ...state.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = + subSpace.subspaceName.toLowerCase(); + + final duplicateIndices = state.subSpaces + .asMap() + .entries + .where((e) => + e.value.subspaceName.toLowerCase() == + lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = + duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + + return Chip( + label: Text(subSpace.subspaceName, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: + ColorsManager.spaceColor)), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate + ? ColorsManager.red + : ColorsManager.transparentColor, + width: 0, ), ), - child: const Icon( - Icons.close, - size: 16, - color: ColorsManager.lightGrayColor, + deleteIcon: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const Icon( + Icons.close, + size: 16, + color: ColorsManager.lightGrayColor, + ), ), - ), - onDeleted: () => context - .read() - .add(RemoveSubSpace(subSpace)), - ), + onDeleted: () => context + .read() + .add(RemoveSubSpace(subSpace)), + ); + }, ), SizedBox( width: 200, child: TextField( - controller: textController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: state.subSpaces.isEmpty - ? 'Please enter the name' - : null, - hintStyle: const TextStyle( - color: ColorsManager.lightGrayColor), - ), - onSubmitted: (value) { - if (value.trim().isNotEmpty) { - context.read().add( - AddSubSpace(SubspaceModel( - subspaceName: value.trim(), - disabled: false))); - textController.clear(); - } - }, - style: const TextStyle( - color: ColorsManager.blackColor), - ), + controller: textController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: state.subSpaces.isEmpty + ? 'Please enter the name' + : null, + hintStyle: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: ColorsManager + .lightGrayColor)), + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + context.read().add( + AddSubSpace(SubspaceModel( + subspaceName: value.trim(), + disabled: false))); + textController.clear(); + } + }, + style: + Theme.of(context).textTheme.bodyMedium), ), - if (state.errorMessage.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - state.errorMessage, - style: const TextStyle( - color: ColorsManager.warningRed, - fontSize: 12, - ), - ), - ), ], ), ), + if (state.errorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(state.errorMessage, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: ColorsManager.warningRed, + )), + ), const SizedBox(height: 16), Row( children: [ Expanded( child: CancelButton( label: 'Cancel', - onPressed: () async {}, + onPressed: () async { + Navigator.of(context).pop(); + }, ), ), const SizedBox(width: 10), Expanded( child: DefaultButton( - onPressed: () async { - final subSpaces = context - .read() - .state - .subSpaces; - onSave!(subSpaces); - Navigator.of(context).pop(); - }, + onPressed: (state.errorMessage.isNotEmpty) + ? null + : () async { + final subSpaces = context + .read() + .state + .subSpaces; + onSave!(subSpaces); + Navigator.of(context).pop(); + }, backgroundColor: ColorsManager.secondaryColor, borderRadius: 10, - foregroundColor: ColorsManager.whiteColors, + foregroundColor: state.errorMessage.isNotEmpty + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, child: const Text('OK'), ), ), 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/create_subspace_model/views/create_subspace_model_dialog.dart b/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart index 4c0cb99f..7a39891b 100644 --- a/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart +++ b/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart @@ -94,12 +94,13 @@ class CreateSubSpaceModelDialog extends StatelessWidget { duplicateIndices.indexOf(index) != 0; return Chip( - label: Text( - subSpace.subspaceName, - style: const TextStyle( - color: ColorsManager.spaceColor, - ), - ), + label: Text(subSpace.subspaceName, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: ColorsManager.spaceColor, + )), backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), @@ -135,28 +136,33 @@ class CreateSubSpaceModelDialog extends StatelessWidget { SizedBox( width: 200, child: TextField( - controller: textController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: state.subSpaces.isEmpty - ? 'Please enter the name' - : null, - hintStyle: const TextStyle( - color: ColorsManager.lightGrayColor), - ), - onSubmitted: (value) { - if (value.trim().isNotEmpty) { - context.read().add( - AddSubSpaceModel( - SubspaceTemplateModel( - subspaceName: value.trim(), - disabled: false))); - textController.clear(); - } - }, - style: const TextStyle( - color: ColorsManager.blackColor), - ), + controller: textController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: state.subSpaces.isEmpty + ? 'Please enter the name' + : null, + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager + .lightGrayColor)), + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + context.read().add( + AddSubSpaceModel( + SubspaceTemplateModel( + subspaceName: value.trim(), + disabled: false))); + textController.clear(); + } + }, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager.blackColor)), ), ], ), @@ -164,13 +170,13 @@ class CreateSubSpaceModelDialog extends StatelessWidget { if (state.errorMessage.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: Text( - state.errorMessage, - style: const TextStyle( - color: ColorsManager.red, - fontSize: 12, - ), - ), + child: Text(state.errorMessage, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: ColorsManager.red, + )), ), const SizedBox(height: 16), Row( diff --git a/lib/pages/spaces_management/helper/tag_helper.dart b/lib/pages/spaces_management/helper/tag_helper.dart index d4a0ea55..041f005f 100644 --- a/lib/pages/spaces_management/helper/tag_helper.dart +++ b/lib/pages/spaces_management/helper/tag_helper.dart @@ -1,5 +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'; @@ -17,23 +20,52 @@ class TagHelper { if (subspaces != null) { for (var subspace in subspaces) { if (subspace.tags != null) { - for (var existingTag in subspace.tags!) { - initialTags.addAll( - subspace.tags!.map( - (tag) => tag.copyWith( - location: subspace.subspaceName, - internalId: existingTag.internalId, - tag: existingTag.tag), + initialTags.addAll( + subspace.tags!.map( + (tag) => tag.copyWith( + location: subspace.subspaceName, + internalId: tag.internalId, + tag: tag.tag, ), - ); - } + ), + ); } } } + return initialTags; } - static Map groupTags(List tags) { + 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; + } + + static Map groupTags(List tags) { final Map groupedTags = {}; for (var tag in tags) { if (tag.product != null) { @@ -78,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 d8b39216..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 @@ -68,8 +68,11 @@ class CreateSpaceModelBloc on((event, emit) { _space = event.spaceTemplate; - emit(CreateSpaceModelLoaded(_space!)); + final String? errorMessage = _checkDuplicateModelName( + event.allModels ?? [], event.spaceTemplate.modelName); + emit(CreateSpaceModelLoaded(_space!, errorMessage: errorMessage)); }); + on((event, emit) { final currentState = state; @@ -132,7 +135,8 @@ class CreateSpaceModelBloc final updatedSpace = currentState.space.copyWith(subspaceModels: updatedSubspaces); - emit(CreateSpaceModelLoaded(updatedSpace)); + emit(CreateSpaceModelLoaded(updatedSpace, + errorMessage: currentState.errorMessage)); } else { emit(CreateSpaceModelError("Space template not initialized")); } @@ -197,94 +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, - 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]; - 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)); - } else {} + final res = await _api.updateSpaceModel( + spaceModelBody, prevSpaceModel?.uuid ?? ''); + + 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')); } @@ -298,10 +309,22 @@ 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) { - for (var prevTag in prevTags!) { + for (var prevTag in prevTags) { final existsInNew = newTags!.any((newTag) => newTag.uuid == prevTag.uuid); if (!existsInNew) { @@ -318,13 +341,16 @@ 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, tag: newTag.tag, + uuid: newTag.uuid == '' ? null : newTag.uuid, productUuid: newTag.product?.uuid)); processedTags.add(newTag.tag); } @@ -350,4 +376,11 @@ class CreateSpaceModelBloc return tagUpdates; } + + String? _checkDuplicateModelName(List allModels, String name) { + if (allModels.contains(name)) { + return "Duplicate Model name"; + } + return null; + } } diff --git a/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart b/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart index 22828941..d0cd245c 100644 --- a/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart +++ b/lib/pages/spaces_management/space_model/bloc/create_space_model_event.dart @@ -14,8 +14,9 @@ class LoadSpaceTemplate extends CreateSpaceModelEvent {} class UpdateSpaceTemplate extends CreateSpaceModelEvent { final SpaceTemplateModel spaceTemplate; + List? allModels; - UpdateSpaceTemplate(this.spaceTemplate); + UpdateSpaceTemplate(this.spaceTemplate,this.allModels); } class CreateSpaceTemplate extends CreateSpaceModelEvent { diff --git a/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart b/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart index 9b61f1b0..ad0770d5 100644 --- a/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart +++ b/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart @@ -1,5 +1,5 @@ class TagBodyModel { - late String uuid; + late String? uuid; late String tag; late final String? productUuid; diff --git a/lib/pages/spaces_management/space_model/models/subspace_template_model.dart b/lib/pages/spaces_management/space_model/models/subspace_template_model.dart index 6c73741b..9c69b4c8 100644 --- a/lib/pages/spaces_management/space_model/models/subspace_template_model.dart +++ b/lib/pages/spaces_management/space_model/models/subspace_template_model.dart @@ -20,7 +20,7 @@ class SubspaceTemplateModel { final String internalId = json['internalId'] ?? const Uuid().v4(); return SubspaceTemplateModel( - uuid: json['uuid'] ?? '', + uuid: json['uuid'], subspaceName: json['subspaceName'] ?? '', internalId: internalId, disabled: json['disabled'] ?? false, diff --git a/lib/pages/spaces_management/space_model/models/tag_model.dart b/lib/pages/spaces_management/space_model/models/tag_model.dart index 48f89167..20bd50e2 100644 --- a/lib/pages/spaces_management/space_model/models/tag_model.dart +++ b/lib/pages/spaces_management/space_model/models/tag_model.dart @@ -1,27 +1,27 @@ +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/space_model/models/create_space_template_body_model.dart'; import 'package:uuid/uuid.dart'; -class TagModel { - String? uuid; - String? tag; - final ProductModel? product; - String internalId; - String? location; - - TagModel( - {this.uuid, - required this.tag, - this.product, - String? internalId, - this.location}) - : internalId = internalId ?? const Uuid().v4(); - +class TagModel extends BaseTag { + TagModel({ + String? uuid, + required String? tag, + ProductModel? product, + String? internalId, + String? location, + }) : super( + uuid: uuid, + tag: tag, + product: product, + internalId: internalId, + location: location, + ); factory TagModel.fromJson(Map json) { final String internalId = json['internalId'] ?? const Uuid().v4(); return TagModel( - uuid: json['uuid'] ?? '', + uuid: json['uuid'] , internalId: internalId, tag: json['tag'] ?? '', product: json['product'] != null @@ -30,16 +30,19 @@ class TagModel { ); } + @override TagModel copyWith( {String? tag, ProductModel? product, + String? uuid, String? location, - String? internalId}) { + String? internalId}) { return TagModel( tag: tag ?? this.tag, product: product ?? this.product, location: location ?? this.location, internalId: internalId ?? this.internalId, + uuid:uuid?? this.uuid ); } @@ -55,7 +58,7 @@ class TagModel { extension TagModelExtensions on TagModel { TagBodyModel toTagBodyModel() { return TagBodyModel() - ..uuid = uuid ?? '' + ..uuid = uuid ..tag = tag ?? '' ..productUuid = product?.uuid; } diff --git a/lib/pages/spaces_management/space_model/view/space_model_page.dart b/lib/pages/spaces_management/space_model/view/space_model_page.dart index ae623e81..b1fce7a1 100644 --- a/lib/pages/spaces_management/space_model/view/space_model_page.dart +++ b/lib/pages/spaces_management/space_model/view/space_model_page.dart @@ -63,7 +63,8 @@ class SpaceModelPage extends StatelessWidget { } // Render existing space model final model = spaceModels[index]; - final otherModel = List.from(allSpaceModelNames); + final otherModel = + List.from(allSpaceModelNames); otherModel.remove(model.modelName); return GestureDetector( onTap: () { @@ -76,6 +77,7 @@ class SpaceModelPage extends StatelessWidget { spaceModel: model, otherSpaceModels: otherModel, pageContext: context, + allSpaceModels: spaceModels, ); }, ); @@ -95,7 +97,10 @@ class SpaceModelPage extends StatelessWidget { return Center( child: Text( 'Error: ${state.message}', - style: const TextStyle(color: ColorsManager.warningRed), + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.warningRed), ), ); } @@ -107,14 +112,14 @@ class SpaceModelPage extends StatelessWidget { double _calculateChildAspectRatio(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; if (screenWidth > 1600) { - return 2; + return 1.5; // Decrease to make cards taller } if (screenWidth > 1200) { - return 3; + return 2.0; } else if (screenWidth > 800) { - return 3.5; + return 2.5; } else { - return 4.0; + return 3.0; } } diff --git a/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart b/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart index 81ecb674..a3ccad7c 100644 --- a/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart +++ b/lib/pages/spaces_management/space_model/widgets/button_content_widget.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class ButtonContentWidget extends StatelessWidget { - final IconData icon; + final IconData? icon; final String label; + final String? svgAssets; - const ButtonContentWidget({ - Key? key, - required this.icon, - required this.label, - }) : super(key: key); + const ButtonContentWidget( + {Key? key, this.icon, required this.label, this.svgAssets}) + : super(key: key); @override Widget build(BuildContext context) { @@ -30,10 +30,20 @@ class ButtonContentWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), child: Row( children: [ - Icon( - icon, - color: ColorsManager.spaceColor, - ), + if (icon != null) + Icon( + icon, + color: ColorsManager.spaceColor, + ), + if (svgAssets != null) + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + svgAssets!, + width: screenWidth * 0.015, // Adjust icon size + height: screenWidth * 0.015, + ), + ), const SizedBox(width: 10), Expanded( child: Text( diff --git a/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart b/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart index c1bea0fd..212400b9 100644 --- a/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart +++ b/lib/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart @@ -22,6 +22,7 @@ class CreateSpaceModelDialog extends StatelessWidget { final SpaceTemplateModel? spaceModel; final BuildContext? pageContext; final List? otherSpaceModels; + final List? allSpaceModels; const CreateSpaceModelDialog( {Key? key, @@ -29,7 +30,8 @@ class CreateSpaceModelDialog extends StatelessWidget { this.allTags, this.spaceModel, this.pageContext, - this.otherSpaceModels}) + this.otherSpaceModels, + this.allSpaceModels}) : super(key: key); @override @@ -50,12 +52,14 @@ class CreateSpaceModelDialog extends StatelessWidget { create: (_) { final bloc = CreateSpaceModelBloc(_spaceModelApi); if (spaceModel != null) { - bloc.add(UpdateSpaceTemplate(spaceModel!)); + bloc.add(UpdateSpaceTemplate(spaceModel!, otherSpaceModels)); } else { - bloc.add(UpdateSpaceTemplate(SpaceTemplateModel( - modelName: '', - subspaceModels: const [], - ))); + bloc.add(UpdateSpaceTemplate( + SpaceTemplateModel( + modelName: '', + subspaceModels: const [], + ), + otherSpaceModels)); } spaceNameController.addListener(() { @@ -98,14 +102,19 @@ class CreateSpaceModelDialog extends StatelessWidget { name: value, allModels: otherSpaceModels ?? [])); }, - style: const TextStyle(color: ColorsManager.blackColor), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.blackColor), decoration: InputDecoration( filled: true, fillColor: ColorsManager.textFieldGreyColor, hintText: 'Please enter the name', errorText: state.errorMessage, - hintStyle: const TextStyle( - color: ColorsManager.lightGrayColor), + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.lightGrayColor), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, @@ -119,7 +128,6 @@ class CreateSpaceModelDialog extends StatelessWidget { ), const SizedBox(height: 16), SubspaceModelCreate( - context, subspaces: state.space.subspaceModels ?? [], onSpaceModelUpdate: (updatedSubspaces) { context @@ -138,6 +146,7 @@ class CreateSpaceModelDialog extends StatelessWidget { spaceNameController: spaceNameController, pageContext: pageContext, otherSpaceModels: otherSpaceModels, + allSpaceModels: allSpaceModels, ), const SizedBox(height: 20), SizedBox( @@ -147,15 +156,19 @@ class CreateSpaceModelDialog extends StatelessWidget { Expanded( child: CancelButton( label: 'Cancel', - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + Navigator.of(context).pop(); + }, ), ), const SizedBox(width: 10), Expanded( child: DefaultButton( - onPressed: state.errorMessage == null || - isNameValid - ? () { + onPressed: ((state.errorMessage != null && + state.errorMessage != '') || + !isNameValid) + ? null + : () { final updatedSpaceTemplate = updatedSpaceModel.copyWith( modelName: @@ -228,13 +241,14 @@ class CreateSpaceModelDialog extends StatelessWidget { } } } - } - : null, + }, backgroundColor: ColorsManager.secondaryColor, borderRadius: 10, - foregroundColor: isNameValid - ? ColorsManager.whiteColors - : ColorsManager.whiteColorsWithOpacity, + foregroundColor: ((state.errorMessage != null && + state.errorMessage != '') || + !isNameValid) + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, child: const Text('OK'), ), ), @@ -246,7 +260,10 @@ class CreateSpaceModelDialog extends StatelessWidget { } else if (state is CreateSpaceModelError) { return Text( 'Error: ${state.message}', - style: const TextStyle(color: ColorsManager.warningRed), + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.warningRed), ); } diff --git a/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart b/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart index e24c7704..f3da4122 100644 --- a/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart +++ b/lib/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart @@ -29,7 +29,7 @@ class DynamicRoomWidget extends StatelessWidget { final TextPainter textPainter = TextPainter( text: TextSpan( text: subspace.subspaceName, - style: const TextStyle(fontSize: 16), + style: Theme.of(context).textTheme.bodyMedium ), textDirection: TextDirection.ltr, )..layout(); diff --git a/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart b/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart index df0fba4f..0056c96f 100644 --- a/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart +++ b/lib/pages/spaces_management/space_model/widgets/space_model_card_widget.dart @@ -31,82 +31,90 @@ class SpaceModelCardWidget extends StatelessWidget { } } - return Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: const Offset(0, 3), + return LayoutBuilder( + builder: (context, constraints) { + bool showOnlyName = constraints.maxWidth < 250; + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - model.modelName, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 10), - Expanded( - child: Row( - children: [ - // Left Container + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.modelName, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (!showOnlyName) ...[ + const SizedBox(height: 10), Expanded( - flex: 1, // Distribute space proportionally - child: Container( - padding: const EdgeInsets.all(8.0), - child: LayoutBuilder( - builder: (context, constraints) { - return Align( - alignment: Alignment.topLeft, - child: DynamicRoomWidget( - subspaceModels: model.subspaceModels, - maxWidth: constraints.maxWidth, - maxHeight: constraints.maxHeight, + child: Row( + children: [ + // Left Container + Expanded( + flex: 1, // Distribute space proportionally + child: Container( + padding: const EdgeInsets.all(8.0), + child: LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Alignment.topLeft, + child: DynamicRoomWidget( + subspaceModels: model.subspaceModels, + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, + ), + ); + }, ), - ); - }, - ), + ), + ), + if (productTagCount.isNotEmpty && + model.subspaceModels != null) + Container( + width: 1.0, + color: ColorsManager.softGray, + margin: const EdgeInsets.symmetric(vertical: 6.0), + ), + Expanded( + flex: 1, // Distribute space proportionally + child: Container( + padding: const EdgeInsets.all(8.0), + child: LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Alignment.topLeft, + child: DynamicProductWidget( + productTagCount: productTagCount, + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight)); + }, + ), + ), + ), + ], ), ), - if (productTagCount.isNotEmpty && model.subspaceModels != null) - Container( - width: 1.0, - color: ColorsManager.softGray, - margin: const EdgeInsets.symmetric(vertical: 6.0), - ), - Expanded( - flex: 1, // Distribute space proportionally - child: Container( - padding: const EdgeInsets.all(8.0), - child: LayoutBuilder( - builder: (context, constraints) { - return Align( - alignment: Alignment.topLeft, - child: DynamicProductWidget( - productTagCount: productTagCount, - maxWidth: constraints.maxWidth, - maxHeight: constraints.maxHeight)); - }, - ), - ), - ), - ], - ), + ] + ], ), - ], - ), + ); + }, ); } } diff --git a/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart b/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart index 0dda53a6..3e13f9c5 100644 --- a/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart +++ b/lib/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart @@ -1,23 +1,41 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -class SubspaceModelCreate extends StatelessWidget { +class SubspaceModelCreate extends StatefulWidget { final List subspaces; final void Function(List newSubspaces)? onSpaceModelUpdate; - const SubspaceModelCreate(BuildContext context, - {Key? key, required this.subspaces, this.onSpaceModelUpdate}) - : super(key: key); + const SubspaceModelCreate({ + Key? key, + required this.subspaces, + this.onSpaceModelUpdate, + }) : super(key: key); + + @override + _SubspaceModelCreateState createState() => _SubspaceModelCreateState(); +} + +class _SubspaceModelCreateState extends State { + late List _subspaces; + String? errorSubspaceId; + + @override + void initState() { + super.initState(); + _subspaces = List.from(widget.subspaces); + } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; return Container( - child: subspaces.isEmpty + child: _subspaces.isEmpty ? TextButton( style: TextButton.styleFrom( overlayColor: ColorsManager.transparentColor, @@ -39,46 +57,37 @@ class SubspaceModelCreate extends StatelessWidget { borderRadius: BorderRadius.circular(15), border: Border.all( color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width + width: 3.0, ), ), child: Wrap( spacing: 8.0, runSpacing: 8.0, children: [ - ...subspaces.map((subspace) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, vertical: 4.0), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: ColorsManager.transparentColor), + ..._subspaces.map((subspace) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SubspaceNameDisplayWidget( + text: subspace.subspaceName, + validateName: (updatedName) { + return !_subspaces.any((s) => + s != subspace && + s.subspaceName == updatedName); + }, + onNameChanged: (updatedName) { + setState(() { + subspace.subspaceName = updatedName; + }); + }, ), - child: Text( - subspace.subspaceName, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.spaceColor), - ), - )), - GestureDetector( + ], + ); + }), + EditChip( onTap: () async { await _openDialog(context, 'Edit Sub-space'); }, - child: Chip( - label: const Text( - 'Edit', - style: TextStyle(color: ColorsManager.spaceColor), - ), - backgroundColor: ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: - const BorderSide(color: ColorsManager.spaceColor), - ), - ), ), ], ), @@ -95,9 +104,16 @@ class SubspaceModelCreate extends StatelessWidget { return CreateSubSpaceModelDialog( isEdit: true, dialogTitle: dialogTitle, - existingSubSpaces: subspaces, + existingSubSpaces: _subspaces, + onUpdate: (subspaceModels) { - onSpaceModelUpdate!(subspaceModels); + setState(() { + _subspaces = subspaceModels; + errorSubspaceId = null; + }); + if (widget.onSpaceModelUpdate != null) { + widget.onSpaceModelUpdate!(subspaceModels); + } }, ); }, diff --git a/lib/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart b/lib/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart new file mode 100644 index 00000000..63edcf8f --- /dev/null +++ b/lib/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SubspaceNameDisplayWidget extends StatefulWidget { + final String text; + final TextStyle? textStyle; + final Color backgroundColor; + final Color borderColor; + final EdgeInsetsGeometry padding; + final BorderRadiusGeometry borderRadius; + final void Function(String updatedName) onNameChanged; + final bool Function(String updatedName) validateName; + + const SubspaceNameDisplayWidget({ + Key? key, + required this.text, + this.textStyle, + this.backgroundColor = ColorsManager.whiteColors, + this.borderColor = ColorsManager.transparentColor, + this.padding = const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + this.borderRadius = const BorderRadius.all(Radius.circular(10)), + required this.onNameChanged, + required this.validateName, + }) : super(key: key); + + @override + _SubspaceNameDisplayWidgetState createState() => + _SubspaceNameDisplayWidgetState(); +} + +class _SubspaceNameDisplayWidgetState extends State { + bool isEditing = false; + late TextEditingController _controller; + late FocusNode _focusNode; + late String previousName; + String? errorText; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.text); + _focusNode = FocusNode(); + previousName = widget.text; + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _handleValidationAndSave() { + final updatedName = _controller.text; + if (widget.validateName(updatedName)) { + setState(() { + errorText = null; + isEditing = false; + previousName = updatedName; + widget.onNameChanged(updatedName); + }); + } else { + setState(() { + errorText = 'Subspace name already exists.'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + setState(() { + isEditing = true; + _focusNode.requestFocus(); + }); + }, + child: Container( + padding: widget.padding, + decoration: BoxDecoration( + color: widget.backgroundColor, + borderRadius: widget.borderRadius, + border: Border.all(color: widget.borderColor), + ), + child: isEditing + ? TextField( + controller: _controller, + focusNode: _focusNode, + style: widget.textStyle ?? + Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + autofocus: true, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 8.0), + ), + onSubmitted: (value) { + _handleValidationAndSave(); + }, + ) + : Text( + widget.text, + style: widget.textStyle ?? + Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + ), + ), + if (errorText != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + errorText!, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.warningRed), + ), + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart b/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart index d4111031..a07f9b29 100644 --- a/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart +++ b/lib/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; @@ -18,6 +19,7 @@ class TagChipDisplay extends StatelessWidget { final TextEditingController spaceNameController; final BuildContext? pageContext; final List? otherSpaceModels; + final List? allSpaceModels; const TagChipDisplay(BuildContext context, {Key? key, @@ -28,7 +30,8 @@ class TagChipDisplay extends StatelessWidget { required this.allTags, required this.spaceNameController, this.pageContext, - this.otherSpaceModels}) + this.otherSpaceModels, + this.allSpaceModels}) : super(key: key); @override @@ -70,9 +73,12 @@ class TagChipDisplay extends StatelessWidget { ), label: Text( 'x${entry.value}', // Show count - style: const TextStyle( - color: ColorsManager.spaceColor, - ), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: + ColorsManager.spaceColor), ), backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( @@ -83,45 +89,31 @@ class TagChipDisplay extends StatelessWidget { ), ), ), - GestureDetector( - onTap: () async { - // Use the Navigator's context for showDialog - final navigatorContext = - Navigator.of(context).overlay?.context; + EditChip(onTap: () async { + // Use the Navigator's context for showDialog + Navigator.of(context).pop(); - if (navigatorContext != null) { - await showDialog( - barrierDismissible: false, - context: navigatorContext, - builder: (context) => AssignTagModelsDialog( - products: products, + await showDialog( + barrierDismissible: false, + context: context, + builder: (context) => AssignTagModelsDialog( + products: products, + allSpaceModels: allSpaceModels, + subspaces: subspaces, + pageContext: pageContext, + allTags: allTags, + spaceModel: spaceModel, + otherSpaceModels: otherSpaceModels, + initialTags: TagHelper.generateInitialTags( subspaces: subspaces, - pageContext: pageContext, - allTags: allTags, - spaceModel: spaceModel, - initialTags: TagHelper.generateInitialTags( - subspaces: subspaces, - spaceTagModels: spaceModel?.tags ?? []), - title: 'Edit Device', - addedProducts: - TagHelper.createInitialSelectedProducts( - spaceModel?.tags ?? [], subspaces), - spaceName: spaceModel?.modelName ?? '', - )); - } - }, - child: Chip( - label: const Text( - 'Edit', - style: TextStyle(color: ColorsManager.spaceColor), - ), - backgroundColor: ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: ColorsManager.spaceColor), - ), - ), - ), + spaceTagModels: spaceModel?.tags ?? []), + title: 'Edit Device', + addedProducts: + TagHelper.createInitialSelectedProducts( + spaceModel?.tags ?? [], subspaces), + spaceName: spaceModel?.modelName ?? '', + )); + }) ], ), ), @@ -141,6 +133,7 @@ class TagChipDisplay extends StatelessWidget { pageContext: pageContext, isCreate: true, spaceModel: spaceModel, + otherSpaceModels: otherSpaceModels, ), ); }, diff --git a/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart b/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart index a9d40147..9d0eac96 100644 --- a/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart +++ b/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.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/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.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'; @@ -26,6 +27,7 @@ class AddDeviceTypeModelWidget extends StatelessWidget { final List? otherSpaceModels; final BuildContext? pageContext; final SpaceTemplateModel? spaceModel; + final List? allSpaceModels; const AddDeviceTypeModelWidget( {super.key, @@ -38,7 +40,8 @@ class AddDeviceTypeModelWidget extends StatelessWidget { required this.isCreate, this.pageContext, this.otherSpaceModels, - this.spaceModel}); + this.spaceModel, + this.allSpaceModels}); @override Widget build(BuildContext context) { @@ -106,6 +109,7 @@ class AddDeviceTypeModelWidget extends StatelessWidget { context: context, builder: (BuildContext dialogContext) { return CreateSpaceModelDialog( + allSpaceModels: allSpaceModels, products: products, allTags: allTags, pageContext: pageContext, @@ -120,7 +124,7 @@ class AddDeviceTypeModelWidget extends StatelessWidget { }, ); } else { - final initialTags = generateInitialTags( + final initialTags = TagHelper.generateInitialTags( spaceTagModels: spaceTagModels, subspaces: subspaces, ); @@ -162,7 +166,8 @@ class AddDeviceTypeModelWidget extends StatelessWidget { : () async { if (state is AddDeviceModelLoaded && state.selectedProducts.isNotEmpty) { - final initialTags = generateInitialTags( + final initialTags = + TagHelper.generateInitialTags( spaceTagModels: spaceTagModels, subspaces: subspaces, ); @@ -175,11 +180,12 @@ class AddDeviceTypeModelWidget extends StatelessWidget { context: context, builder: (context) => AssignTagModelsDialog( products: products, + allSpaceModels: allSpaceModels, subspaces: subspaces, addedProducts: state.selectedProducts, allTags: allTags, spaceName: spaceName, - initialTags: state.initialTag, + initialTags: initialTags, otherSpaceModels: otherSpaceModels, title: dialogTitle, spaceModel: spaceModel, @@ -200,29 +206,4 @@ class AddDeviceTypeModelWidget extends StatelessWidget { ), ); } - - List generateInitialTags({ - List? spaceTagModels, - List? subspaces, - }) { - final List initialTags = []; - - if (spaceTagModels != null) { - initialTags.addAll(spaceTagModels); - } - - if (subspaces != null) { - for (var subspace in subspaces) { - if (subspace.tags != null) { - initialTags.addAll( - subspace.tags!.map( - (tag) => tag.copyWith(location: subspace.subspaceName), - ), - ); - } - } - } - - return initialTags; - } } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 2a2d42ad..c4877c98 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -4,7 +4,9 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subs import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; import 'package:syncrow_web/utils/constants/temp_const.dart'; @@ -154,7 +156,7 @@ class CommunitySpaceManagementApi { .replaceAll('{spaceId}', spaceId) .replaceAll('{projectId}', TempConst.projectId), expectedResponseModel: (json) { - return SpaceModel.fromJson(json); + return SpaceModel.fromJson(json['data']); }, ); return response; @@ -210,7 +212,7 @@ class CommunitySpaceManagementApi { } } - Future updateSpace({ + Future updateSpace({ required String communityId, required spaceId, required String name, @@ -219,6 +221,8 @@ class CommunitySpaceManagementApi { String? direction, bool isPrivate = false, required Offset position, + List? tags, + List? subspaces, }) async { try { final body = { @@ -228,6 +232,8 @@ class CommunitySpaceManagementApi { 'y': position.dy, 'direction': direction, 'icon': icon, + 'subspace': subspaces, + 'tags': tags, }; if (parentId != null) { body['parentUuid'] = parentId; @@ -240,13 +246,13 @@ class CommunitySpaceManagementApi { .replaceAll('{projectId}', TempConst.projectId), body: body, expectedResponseModel: (json) { - return SpaceModel.fromJson(json['data']); + return json['success'] ?? false; }, ); return response; } catch (e) { - debugPrint('Error creating space: $e'); - return null; + debugPrint('Error updating space: $e'); + return false; } } 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 a9deb3c7..d5d216c5 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -259,6 +259,7 @@ class Assets { static const String delete = 'assets/icons/delete.svg'; static const String edit = 'assets/icons/edit.svg'; + static const String editSpace = 'assets/icons/edit_space.svg'; //assets/icons/routine/tab_to_run.svg static const String tabToRun = 'assets/icons/routine/tab_to_run.svg'; @@ -398,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