From 31019602016eb76ec1dd65fca3ed02e15fc19b44 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:59 +0300 Subject: [PATCH] Enhance Space Management Features with Tag Assignment Improvements: - Introduced UUID for ProductAllocation to ensure unique identification. - Refactored AssignTagsDialog to manage tag assignments and validation more effectively, including error handling for empty tags and duplicate tag usage. - Updated AssignTagsTable to support dynamic product allocation management and improved UI interactions. - Enhanced AddDeviceTypeWidget to maintain selected products and handle increment/decrement actions, improving user experience during device type selection. - Added AssignTagsErrorMessages widget for better error visibility in tag assignment process. --- .../domain/models/space_details_model.dart | 13 +- .../widgets/space_details_devices_box.dart | 52 +++-- .../widgets/add_device_type_widget.dart | 40 +++- .../widgets/assign_tags_dialog.dart | 212 +++++++++++++++++- .../widgets/assign_tags_error_messages.dart | 29 +++ .../widgets/assign_tags_table.dart | 121 +++++----- .../widgets/product_tag_field.dart | 129 +++++------ .../presentation/widgets/products_grid.dart | 50 +++-- 8 files changed, 463 insertions(+), 183 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 5ea10e4a..b3e436b1 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:uuid/uuid.dart'; class SpaceDetailsModel extends Equatable { final String uuid; @@ -31,8 +32,8 @@ class SpaceDetailsModel extends Equatable { spaceName: json['spaceName'] as String, icon: json['icon'] as String, productAllocations: (json['productAllocations'] as List) - .map((e) => ProductAllocation.fromJson(e as Map)) - .toList(), + .map((e) => ProductAllocation.fromJson(e as Map)) + .toList(), subspaces: (json['subspaces'] as List) .map((e) => Subspace.fromJson(e as Map)) .toList(), @@ -70,16 +71,19 @@ class SpaceDetailsModel extends Equatable { } class ProductAllocation extends Equatable { + final String uuid; final Product product; final Tag tag; const ProductAllocation({ + required this.uuid, required this.product, required this.tag, }); factory ProductAllocation.fromJson(Map json) { return ProductAllocation( + uuid: json['uuid'] as String? ?? const Uuid().v4(), product: Product.fromJson(json['product'] as Map), tag: Tag.fromJson(json['tag'] as Map), ); @@ -87,23 +91,26 @@ class ProductAllocation extends Equatable { Map toJson() { return { + 'uuid': uuid, 'product': product.toJson(), 'tag': tag.toJson(), }; } ProductAllocation copyWith({ + String? uuid, Product? product, Tag? tag, }) { return ProductAllocation( + uuid: uuid ?? this.uuid, product: product ?? this.product, tag: tag ?? this.tag, ); } @override - List get props => [product, tag]; + List get props => [uuid, product, tag]; } class Subspace extends Equatable { diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index 3dfd919c..cf65dbb6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -19,11 +21,18 @@ class SpaceDetailsDevicesBox extends StatelessWidget { @override Widget build(BuildContext context) { - final productAllocations = space.productAllocations; - final subspaces = space.subspaces; - final isAnySubspaceHasProductAllocations = - subspaces.any((subspace) => subspace.productAllocations.isNotEmpty); - if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) { + final allAllocations = [ + ...space.productAllocations, + ...space.subspaces.expand((s) => s.productAllocations), + ]; + + if (allAllocations.isNotEmpty) { + final productCounts = {}; + for (final allocation in allAllocations) { + final productType = allocation.product.productType; + productCounts[productType] = (productCounts[productType] ?? 0) + 1; + } + return Container( width: double.infinity, padding: const EdgeInsets.all(8), @@ -39,20 +48,23 @@ class SpaceDetailsDevicesBox extends StatelessWidget { spacing: 8.0, runSpacing: 8.0, children: [ - ...productAllocations.map( - (entry) => Chip( + ...productCounts.entries.map((entry) { + final productType = entry.key; + final count = entry.value; + return Chip( avatar: SizedBox( width: 24, height: 24, child: SvgPicture.asset( - _getDeviceIcon(entry.product.productType), + _getDeviceIcon(productType), fit: BoxFit.contain, ), ), label: Text( - entry.product.productType, - style: context.textTheme.bodySmall - ?.copyWith(color: ColorsManager.spaceColor), + 'x$count', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.spaceColor, + ), ), backgroundColor: ColorsManager.whiteColors, shape: RoundedRectangleBorder( @@ -61,11 +73,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget { color: ColorsManager.spaceColor, ), ), - ), - ), - EditChip( - onTap: () => _showAssignTagsDialog(context), - ), + ); + }), + EditChip(onTap: () => _showAssignTagsDialog(context)), ], ), ); @@ -87,10 +97,16 @@ class SpaceDetailsDevicesBox extends StatelessWidget { } void _showAssignTagsDialog(BuildContext context) { - showDialog( + showDialog( context: context, builder: (context) => AssignTagsDialog(space: space), - ); + ).then((resultSpace) { + if (resultSpace != null) { + if (context.mounted) { + context.read().add(UpdateSpaceDetails(resultSpace)); + } + } + }); } String _getDeviceIcon(String productType) => diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index f77f10b5..4c9990ae 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart'; @@ -8,9 +9,33 @@ import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class AddDeviceTypeWidget extends StatelessWidget { +class AddDeviceTypeWidget extends StatefulWidget { const AddDeviceTypeWidget({super.key}); + @override + State createState() => _AddDeviceTypeWidgetState(); +} + +class _AddDeviceTypeWidgetState extends State { + final Map _selectedProducts = {}; + + void _onIncrement(Product product) { + setState(() { + _selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1; + }); + } + + void _onDecrement(Product product) { + setState(() { + if ((_selectedProducts[product] ?? 0) > 0) { + _selectedProducts[product] = _selectedProducts[product]! - 1; + if (_selectedProducts[product] == 0) { + _selectedProducts.remove(product); + } + } + }); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -22,10 +47,12 @@ class AddDeviceTypeWidget extends StatelessWidget { backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( builder: (context, state) => switch (state) { - ProductsInitial() => _buildLoading(context), - ProductsLoading() => _buildLoading(context), + ProductsInitial() || ProductsLoading() => _buildLoading(context), ProductsLoaded(:final products) => ProductsGrid( products: products, + selectedProducts: _selectedProducts, + onIncrement: _onIncrement, + onDecrement: _onDecrement, ), ProductsFailure(:final errorMessage) => _buildFailure( context, @@ -35,7 +62,12 @@ class AddDeviceTypeWidget extends StatelessWidget { ), actions: [ SpaceDetailsActionButtons( - onSave: () {}, + onSave: () { + final result = _selectedProducts.entries + .expand((entry) => List.generate(entry.value, (_) => entry.key)) + .toList(); + Navigator.of(context).pop(result); + }, onCancel: Navigator.of(context).pop, saveButtonLabel: 'Next', ), diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart index 25f08d30..3cab4abe 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -1,36 +1,230 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:uuid/uuid.dart'; -class AssignTagsDialog extends StatelessWidget { +class AssignTagsDialog extends StatefulWidget { const AssignTagsDialog({required this.space, super.key}); final SpaceDetailsModel space; + @override + State createState() => _AssignTagsDialogState(); +} + +class _AssignTagsDialogState extends State { + late SpaceDetailsModel _space; + final Map _validationErrors = {}; + + @override + void initState() { + super.initState(); + _space = widget.space.copyWith( + productAllocations: + widget.space.productAllocations.map((e) => e.copyWith()).toList(), + subspaces: widget.space.subspaces + .map( + (s) => s.copyWith( + productAllocations: + s.productAllocations.map((e) => e.copyWith()).toList(), + ), + ) + .toList(), + ); + _validateAllTags(); + } + + void _validateAllTags() { + final newErrors = {}; + final allAllocations = [ + ..._space.productAllocations, + ..._space.subspaces.expand((s) => s.productAllocations), + ]; + + final allocationsByProductType = >{}; + for (final allocation in allAllocations) { + (allocationsByProductType[allocation.product.productType] ??= []) + .add(allocation); + } + + for (final productType in allocationsByProductType.keys) { + final allocations = allocationsByProductType[productType]!; + final tagCounts = {}; + + for (final allocation in allocations) { + final tagName = allocation.tag.name.trim().toLowerCase(); + if (tagName.isEmpty) { + newErrors[allocation.uuid] = + 'Tag for ${allocation.product.name} cannot be empty.'; + } else { + tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1; + } + } + + for (final allocation in allocations) { + final tagName = allocation.tag.name.trim().toLowerCase(); + if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) { + newErrors[allocation.uuid] = + 'Tag "${allocation.tag.name}" is used by multiple $productType devices.'; + } + } + } + + setState(() { + _validationErrors + ..clear() + ..addAll(newErrors); + }); + } + + void _handleTagChange(String allocationUuid, Tag newTag) { + setState(() { + var index = + _space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + final allocation = _space.productAllocations[index]; + _space.productAllocations[index] = allocation.copyWith(tag: newTag); + } else { + for (final subspace in _space.subspaces) { + index = subspace.productAllocations + .indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + final allocation = subspace.productAllocations[index]; + subspace.productAllocations[index] = allocation.copyWith(tag: newTag); + break; + } + } + } + }); + _validateAllTags(); + } + + void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) { + setState(() { + ProductAllocation? allocationToMove; + + var index = + _space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + allocationToMove = _space.productAllocations.removeAt(index); + } else { + for (final subspace in _space.subspaces) { + index = subspace.productAllocations + .indexWhere((pa) => pa.uuid == allocationUuid); + if (index != -1) { + allocationToMove = subspace.productAllocations.removeAt(index); + break; + } + } + } + + if (allocationToMove == null) return; + + if (newSubspaceUuid == null) { + _space.productAllocations.add(allocationToMove); + } else { + _space.subspaces + .firstWhere((s) => s.uuid == newSubspaceUuid) + .productAllocations + .add(allocationToMove); + } + }); + } + + void _handleProductDelete(String allocationUuid) { + setState(() { + _space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid); + + for (final subspace in _space.subspaces) { + subspace.productAllocations.removeWhere( + (pa) => pa.uuid == allocationUuid, + ); + } + }); + _validateAllTags(); + } + @override Widget build(BuildContext context) { + final allProductAllocations = [ + ..._space.productAllocations, + ..._space.subspaces.expand((s) => s.productAllocations), + ]; + + final productLocations = {}; + for (final pa in _space.productAllocations) { + productLocations[pa.uuid] = null; + } + for (final subspace in _space.subspaces) { + for (final pa in subspace.productAllocations) { + productLocations[pa.uuid] = subspace.uuid; + } + } + + final hasErrors = _validationErrors.isNotEmpty; + return AlertDialog( - title: const Text('Assign Tags'), + title: const SelectableText('Assign Tags'), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: context.screenWidth * 0.6, minWidth: context.screenWidth * 0.6, maxHeight: context.screenHeight * 0.8, ), - child: AssignTagsTable(productAllocations: space.productAllocations), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: AssignTagsTable( + productAllocations: allProductAllocations, + subspaces: _space.subspaces, + productLocations: productLocations, + onTagSelected: _handleTagChange, + onLocationSelected: _handleLocationChange, + onProductDeleted: _handleProductDelete, + ), + ), + if (hasErrors) + AssignTagsErrorMessages( + errorMessages: _validationErrors.values.toSet().toList(), + ), + ], + ), ), actions: [ SpaceDetailsActionButtons( - onSave: () {}, - onCancel: () => showDialog( - context: context, - builder: (context) => const AddDeviceTypeWidget(), - ), + onSave: hasErrors ? null : () => Navigator.of(context).pop(_space), + onCancel: () async { + final newProducts = await showDialog>( + context: context, + builder: (context) => const AddDeviceTypeWidget(), + ); + + if (newProducts == null || newProducts.isEmpty) return; + + setState(() { + for (final product in newProducts) { + _space.productAllocations.add( + ProductAllocation( + uuid: const Uuid().v4(), + product: product, + tag: Tag.empty(), + ), + ); + } + }); + _validateAllTags(); + }, cancelButtonLabel: 'Add New Device', - ), + ) ], ); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart new file mode 100644 index 00000000..9b0fd478 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AssignTagsErrorMessages extends StatelessWidget { + const AssignTagsErrorMessages({super.key, required this.errorMessages}); + + final List errorMessages; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: errorMessages + .map( + (error) => Text( + '- $error', + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart index 3d92109d..6e7e2097 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -3,50 +3,35 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/dialog_dropdown.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -import 'package:uuid/uuid.dart'; -class AssignTagsTable extends StatefulWidget { +class AssignTagsTable extends StatelessWidget { const AssignTagsTable({ required this.productAllocations, + required this.subspaces, + required this.productLocations, + required this.onTagSelected, + required this.onLocationSelected, + required this.onProductDeleted, super.key, }); final List productAllocations; + final List subspaces; + final Map productLocations; + final void Function(String, Tag) onTagSelected; + final void Function(String, String?) onLocationSelected; + final void Function(String) onProductDeleted; - @override - State createState() => _AssignTagsTableState(); -} - -class _AssignTagsTableState extends State { - List _controllers = []; - - @override - void initState() { - super.initState(); - _controllers = List.generate( - widget.productAllocations.length, - (index) => TextEditingController( - text: widget.productAllocations[index].product.name, - ), - ); - } - - @override - void dispose() { - for (final controller in _controllers) { - controller.dispose(); - } - super.dispose(); - } - - DataColumn _buildDataColumn(String label) { + DataColumn _buildDataColumn(BuildContext context, String label) { return DataColumn( - label: SelectableText(label, style: context.textTheme.bodyMedium)); + label: SelectableText(label, style: context.textTheme.bodyMedium), + ); } @override @@ -70,19 +55,19 @@ class _AssignTagsTableState extends State { headingRowColor: WidgetStateProperty.all( ColorsManager.dataHeaderGrey, ), - key: ValueKey(widget.productAllocations.length), + key: ValueKey(productAllocations.length), border: TableBorder.all( color: ColorsManager.dataHeaderGrey, width: 1, borderRadius: BorderRadius.circular(20), ), columns: [ - _buildDataColumn('#'), - _buildDataColumn('Device'), - _buildDataColumn('Tag'), - _buildDataColumn('Location'), + _buildDataColumn(context, '#'), + _buildDataColumn(context, 'Device'), + _buildDataColumn(context, 'Tag'), + _buildDataColumn(context, 'Location'), ], - rows: widget.productAllocations.isEmpty + rows: productAllocations.isEmpty ? [ DataRow( cells: [ @@ -102,11 +87,33 @@ class _AssignTagsTableState extends State { ], ), ] - : List.generate(widget.productAllocations.length, (index) { - final productAllocation = widget.productAllocations[index]; - final controller = _controllers[index]; + : List.generate(productAllocations.length, (index) { + final productAllocation = productAllocations[index]; + final allocationUuid = productAllocation.uuid; + + final availableTags = tags + .where( + (tag) => + !productAllocations + .where((p) => + p.product.productType == + productAllocation.product.productType) + .map((p) => p.tag.name.toLowerCase()) + .contains(tag.name.toLowerCase()) || + tag.uuid == productAllocation.tag.uuid, + ) + .toList(); + + final currentLocationUuid = + productLocations[allocationUuid]; + final currentLocationName = currentLocationUuid == null + ? 'Main Space' + : subspaces + .firstWhere((s) => s.uuid == currentLocationUuid) + .name; return DataRow( + key: ValueKey(allocationUuid), cells: [ DataCell(Text((index + 1).toString())), DataCell( @@ -136,7 +143,7 @@ class _AssignTagsTableState extends State { size: 16, ), onPressed: () { - // TODO: Delete the product allocation + onProductDeleted(allocationUuid); }, tooltip: 'Delete Tag', padding: EdgeInsets.zero, @@ -151,14 +158,13 @@ class _AssignTagsTableState extends State { alignment: Alignment.centerLeft, width: double.infinity, child: ProductTagField( - key: ValueKey( - 'dropdown_${const Uuid().v4()}_$index'), + key: ValueKey('dropdown_$allocationUuid'), productName: productAllocation.product.uuid, - initialValue: null, - onSelected: (value) { - controller.text = value.name; + initialValue: productAllocation.tag, + onSelected: (newTag) { + onTagSelected(allocationUuid, newTag); }, - items: tags, + items: availableTags, ), ), ), @@ -166,13 +172,22 @@ class _AssignTagsTableState extends State { SizedBox( width: double.infinity, child: DialogDropdown( - items: const [], - // items: widget.locations, - selectedValue: - productAllocation.tag.name.isEmpty - ? 'Main Space' - : productAllocation.tag.name, - onSelected: (value) {}, + items: [ + 'Main Space', + ...subspaces.map((s) => s.name) + ], + selectedValue: currentLocationName, + onSelected: (newLocationName) { + final newSubspaceUuid = newLocationName == + 'Main Space' + ? null + : subspaces + .firstWhere( + (s) => s.name == newLocationName) + .uuid; + onLocationSelected( + allocationUuid, newSubspaceUuid); + }, )), ), ], diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart index 04c82370..8bbf379d 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart @@ -26,42 +26,44 @@ class _ProductTagFieldState extends State { OverlayEntry? _overlayEntry; final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); - List _filteredItems = []; @override void initState() { super.initState(); _controller.text = widget.initialValue?.name ?? ''; - - _filterItems(); - - _focusNode.addListener(() { - if (!_focusNode.hasFocus) { - final selectedTag = _filteredItems.firstWhere( - (tag) => tag.name == _controller.text, - orElse: () => Tag( - name: _controller.text, - uuid: '', - createdAt: '', - updatedAt: '', - ), - ); - widget.onSelected(selectedTag); - _closeDropdown(); - } - }); + _focusNode.addListener(_handleFocusChange); } @override void dispose() { + _focusNode.removeListener(_handleFocusChange); _controller.dispose(); _focusNode.dispose(); + _overlayEntry?.remove(); _overlayEntry = null; - _isOpen = false; super.dispose(); } - void _filterItems() => setState(() => _filteredItems = widget.items); + void _handleFocusChange() { + if (!_focusNode.hasFocus) { + _submit(_controller.text); + } + } + + void _submit(String value) { + final lowerCaseValue = value.toLowerCase(); + final selectedTag = widget.items.firstWhere( + (tag) => tag.name.toLowerCase() == lowerCaseValue, + orElse: () => Tag( + name: value, + uuid: '', + createdAt: '', + updatedAt: '', + ), + ); + widget.onSelected(selectedTag); + _closeDropdown(); + } void _toggleDropdown() { if (_isOpen) { @@ -74,14 +76,14 @@ class _ProductTagFieldState extends State { void _openDropdown() { _overlayEntry = _createOverlayEntry(); Overlay.of(context).insert(_overlayEntry!); - _isOpen = true; + setState(() => _isOpen = true); } void _closeDropdown() { - if (_isOpen && _overlayEntry != null) { - _overlayEntry!.remove(); + if (_isOpen) { + _overlayEntry?.remove(); _overlayEntry = null; - _isOpen = false; + setState(() => _isOpen = false); } } @@ -103,24 +105,7 @@ class _ProductTagFieldState extends State { child: TextFormField( controller: _controller, focusNode: _focusNode, - onFieldSubmitted: (value) { - final selectedTag = _filteredItems.firstWhere( - (tag) => tag.name == value, - orElse: () => - Tag(name: value, uuid: '', createdAt: '', updatedAt: '')); - widget.onSelected(selectedTag); - _closeDropdown(); - }, - onTapOutside: (event) { - widget.onSelected(_filteredItems.firstWhere( - (tag) => tag.name == _controller.text, - orElse: () => Tag( - name: _controller.text, - uuid: '', - createdAt: '', - updatedAt: ''))); - _closeDropdown(); - }, + onFieldSubmitted: _submit, style: context.textTheme.bodyMedium, decoration: const InputDecoration( hintText: 'Enter or Select a tag', @@ -159,41 +144,33 @@ class _ProductTagFieldState extends State { child: Container( color: ColorsManager.whiteColors, constraints: const BoxConstraints(maxHeight: 200.0), - child: StatefulBuilder( - builder: (context, setStateDropdown) { - return ListView.builder( - shrinkWrap: true, - itemCount: _filteredItems.length, - itemBuilder: (context, index) { - final tag = _filteredItems[index]; - - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ColorsManager.lightGrayBorderColor, - width: 1.0, - ), - ), + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.items.length, + itemBuilder: (context, index) { + final tag = widget.items[index]; + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 1.0, ), - child: ListTile( - title: Text( - tag.name, - style: context.textTheme.bodyMedium?.copyWith( - color: ColorsManager.textPrimaryColor, - ), - ), - onTap: () { - _controller.text = tag.name; - widget.onSelected(tag); - setState(() { - _filteredItems.remove(tag); - }); - _closeDropdown(); - }, + ), + ), + child: ListTile( + title: Text( + tag.name, + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textPrimaryColor, ), - ); - }, + ), + onTap: () { + _controller.text = tag.name; + _submit(tag.name); + _closeDropdown(); + }, + ), ); }, ), diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart index 7fe5ec26..53e59bde 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart @@ -5,9 +5,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ProductsGrid extends StatelessWidget { - const ProductsGrid({required this.products, super.key}); + const ProductsGrid({ + required this.products, + required this.selectedProducts, + required this.onIncrement, + required this.onDecrement, + super.key, + }); final List products; + final Map selectedProducts; + final void Function(Product) onIncrement; + final void Function(Product) onDecrement; @override Widget build(BuildContext context) { @@ -25,26 +34,27 @@ class ProductsGrid extends StatelessWidget { color: ColorsManager.textFieldGreyColor, borderRadius: BorderRadius.circular(8), ), - child: Expanded( - child: GridView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - mainAxisSpacing: 6, - crossAxisSpacing: 4, - childAspectRatio: 0.8, - ), - itemCount: products.length, - itemBuilder: (context, index) => ProductTypeCard( - product: products[index], - count: 0, - onIncrement: () {}, - onDecrement: () {}, - ), + child: GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 6, + crossAxisSpacing: 4, + childAspectRatio: 0.8, + ), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ProductTypeCard( + product: product, + count: selectedProducts[product] ?? 0, + onIncrement: () => onIncrement(product), + onDecrement: () => onDecrement(product), + ); + }, ), ), );