From 97e3fb68bfff825e658df659e024dd8873791ee3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 14:49:10 +0300 Subject: [PATCH 01/15] Enhance Product Model and SpaceDetailsDevicesBox: - Added 'productType' field to Product model for improved data representation. - Updated JSON parsing in Product model to handle 'prodType'. - Refactored SpaceDetailsDevicesBox to utilize productType for dynamic device icon rendering, enhancing UI clarity and maintainability. --- .../products/domain/models/product.dart | 11 ++- .../widgets/space_details_devices_box.dart | 87 ++++++++++++------- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/lib/pages/space_management_v2/modules/products/domain/models/product.dart b/lib/pages/space_management_v2/modules/products/domain/models/product.dart index cd837121..aac332d6 100644 --- a/lib/pages/space_management_v2/modules/products/domain/models/product.dart +++ b/lib/pages/space_management_v2/modules/products/domain/models/product.dart @@ -3,16 +3,18 @@ import 'package:equatable/equatable.dart'; class Product extends Equatable { final String uuid; final String name; - + final String productType; const Product({ required this.uuid, required this.name, + required this.productType, }); factory Product.fromJson(Map json) { return Product( - uuid: json['uuid'] as String, - name: json['name'] as String, + uuid: json['uuid'] as String? ?? '', + name: json['name'] as String? ?? '', + productType: json['prodType'] as String? ?? '', ); } @@ -20,9 +22,10 @@ class Product extends Equatable { return { 'uuid': uuid, 'name': name, + 'productType': productType, }; } @override - List get props => [uuid, name]; + List get props => [uuid, name, productType]; } 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 5b21bd61..fba905dc 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,12 @@ import 'package:flutter/material.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/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/enum/device_types.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDevicesBox extends StatelessWidget { const SpaceDetailsDevicesBox({ @@ -35,37 +38,30 @@ class SpaceDetailsDevicesBox extends StatelessWidget { spacing: 8.0, runSpacing: 8.0, children: [ - // Combine tags from spaceModel and subspaces - // ...TagHelper.groupTags([ - // ...?tags, - // ...?subspaces?.expand((subspace) => subspace.tags ?? []) - // ]).entries.map( - // (entry) => Chip( - // avatar: SizedBox( - // width: 24, - // height: 24, - // child: SvgPicture.asset( - // entry.key.icon ?? 'assets/icons/gateway.svg', - // fit: BoxFit.contain, - // ), - // ), - // label: Text( - // 'x${entry.value}', - // style: Theme.of(context) - // .textTheme - // .bodySmall - // ?.copyWith(color: ColorsManager.spaceColor), - // ), - // backgroundColor: ColorsManager.whiteColors, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(16), - // side: const BorderSide( - // color: ColorsManager.spaceColor, - // ), - // ), - // ), - // ), - + ...productAllocations.map( + (entry) => Chip( + avatar: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + _getDeviceIcon(entry.product.productType), + fit: BoxFit.contain, + ), + ), + label: Text( + entry.product.productType, + style: context.textTheme.bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor, + ), + ), + ), + ), EditChip( onTap: () {}, ), @@ -83,10 +79,37 @@ class SpaceDetailsDevicesBox extends StatelessWidget { child: ButtonContentWidget( svgAssets: Assets.addIcon, label: 'Add Devices', - // disabled: isTagsAndSubspaceModelDisabled, ), ), ); } } + + String _getDeviceIcon(String productType) => + switch (devicesTypesMap[productType]) { + DeviceType.LightBulb => Assets.lightBulb, + DeviceType.CeilingSensor => Assets.sensors, + DeviceType.AC => Assets.ac, + DeviceType.DoorLock => Assets.doorLock, + DeviceType.Curtain => Assets.curtain, + DeviceType.ThreeGang => Assets.gangSwitch, + DeviceType.Gateway => Assets.gateway, + DeviceType.OneGang => Assets.oneGang, + DeviceType.TwoGang => Assets.twoGang, + DeviceType.WH => Assets.waterHeater, + DeviceType.DoorSensor => Assets.openCloseDoor, + DeviceType.GarageDoor => Assets.openedDoor, + DeviceType.WaterLeak => Assets.waterLeakNormal, + DeviceType.Curtain2 => Assets.curtainIcon, + DeviceType.Blind => Assets.curtainIcon, + DeviceType.WallSensor => Assets.sensors, + DeviceType.DS => Assets.openCloseDoor, + DeviceType.OneTouch => Assets.gangSwitch, + DeviceType.TowTouch => Assets.gangSwitch, + DeviceType.ThreeTouch => Assets.gangSwitch, + DeviceType.NCPS => Assets.sensors, + DeviceType.PC => Assets.powerClamp, + DeviceType.Other => Assets.blackLogo, + null => Assets.blackLogo, + }; } From cebce2ce7f71b5f18684617f9f8bdb4d84e4a0f9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 14:50:24 +0300 Subject: [PATCH 02/15] Update SpaceDetailsModel: Change default icon from villa to location for improved representation of space details. --- .../space_details/domain/models/space_details_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 737d5060..5ea10e4a 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 @@ -21,7 +21,7 @@ class SpaceDetailsModel extends Equatable { factory SpaceDetailsModel.empty() => const SpaceDetailsModel( uuid: '', spaceName: '', - icon: Assets.villa, + icon: Assets.location, productAllocations: [], subspaces: [], ); From bcd0ae4a2abcdcc6fefcec82e60e77a979cc88f9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 16:44:26 +0300 Subject: [PATCH 03/15] Refactor Products Module: - Introduced ProductsBloc and updated ProductsService to remove LoadProductsParam, simplifying the product loading process. - Updated RemoteProductsService to utilize a new API endpoint for fetching products. - Adjusted ProductsEvent and ProductsState to reflect changes in the loading mechanism, enhancing maintainability and clarity in the products management flow. --- .../main_module/views/space_management_page.dart | 7 +++++++ .../data/services/remote_products_service.dart | 15 ++++++--------- .../domain/params/load_products_param.dart | 11 ----------- .../domain/services/products_service.dart | 3 +-- .../products/presentation/bloc/products_bloc.dart | 11 +++++------ .../presentation/bloc/products_event.dart | 7 +------ .../presentation/bloc/products_state.dart | 6 +++--- 7 files changed, 23 insertions(+), 37 deletions(-) delete mode 100644 lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 05768035..1672b821 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.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/data/services/remote_space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -33,6 +35,11 @@ class SpaceManagementPage extends StatelessWidget { RemoteSpaceDetailsService(httpService: HTTPService()), ), ), + BlocProvider( + create: (context) => ProductsBloc( + RemoteProductsService(HTTPService()), + ), + ), ], child: WebScaffold( appBarTitle: Text( diff --git a/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart b/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart index 6e501b44..a01419fe 100644 --- a/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart +++ b/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart @@ -1,9 +1,9 @@ import 'package:dio/dio.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/domain/params/load_products_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteProductsService implements ProductsService { const RemoteProductsService(this._httpService); @@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService { static const _defaultErrorMessage = 'Failed to load devices'; @override - Future> getProducts(LoadProductsParam param) async { + Future> getProducts() async { try { final response = await _httpService.get( - path: 'devices', - queryParameters: { - 'spaceUuid': param.spaceUuid, - if (param.type != null) 'type': param.type, - if (param.status != null) 'status': param.status, - }, + path: ApiEndpoints.listProducts, expectedResponseModel: (data) { - return (data as List) + final json = data as Map; + final products = json['data'] as List; + return products .map((e) => Product.fromJson(e as Map)) .toList(); }, diff --git a/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart b/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart deleted file mode 100644 index 87194ae7..00000000 --- a/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart +++ /dev/null @@ -1,11 +0,0 @@ -class LoadProductsParam { - final String spaceUuid; - final String? type; - final String? status; - - const LoadProductsParam({ - required this.spaceUuid, - this.type, - this.status, - }); -} diff --git a/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart b/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart index 18554382..f6d41d0c 100644 --- a/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart +++ b/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart @@ -1,6 +1,5 @@ import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart'; abstract class ProductsService { - Future> getProducts(LoadProductsParam param); + Future> getProducts(); } diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart index 1ce6ae89..0e85f1c7 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; 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/products/domain/params/load_products_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -9,20 +8,20 @@ part 'products_event.dart'; part 'products_state.dart'; class ProductsBloc extends Bloc { - final ProductsService _deviceService; - - ProductsBloc(this._deviceService) : super(ProductsInitial()) { + ProductsBloc(this._productsService) : super(ProductsInitial()) { on(_onLoadProducts); } + final ProductsService _productsService; + Future _onLoadProducts( LoadProducts event, Emitter emit, ) async { emit(ProductsLoading()); try { - final devices = await _deviceService.getProducts(event.param); - emit(ProductsLoaded(devices)); + final products = await _productsService.getProducts(); + emit(ProductsLoaded(products)); } on APIException catch (e) { emit(ProductsFailure(e.message)); } catch (e) { diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart index 971b6d27..7bc14795 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart @@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable { } final class LoadProducts extends ProductsEvent { - const LoadProducts(this.param); - - final LoadProductsParam param; - - @override - List get props => [param]; + const LoadProducts(); } diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart index d5622cd3..78cee7ab 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart @@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState { } final class ProductsFailure extends ProductsState { - final String message; + final String errorMessage; - const ProductsFailure(this.message); + const ProductsFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } From e234c9f3b2a3bc5acaedde5bec7650d3523cf815 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 16:44:40 +0300 Subject: [PATCH 04/15] Enhance SpaceDetailsActionButtons: Introduced customizable button labels for save and cancel actions, improving flexibility and user experience. Updated button implementations to utilize these new labels, enhancing maintainability and adherence to Clean Architecture principles. --- .../widgets/space_details_action_buttons.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart index 8518227f..8d7d2e29 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -8,10 +8,14 @@ class SpaceDetailsActionButtons extends StatelessWidget { super.key, required this.onSave, required this.onCancel, + this.saveButtonLabel = 'OK', + this.cancelButtonLabel = 'Cancel', }); final VoidCallback onCancel; final VoidCallback? onSave; + final String saveButtonLabel; + final String cancelButtonLabel; @override Widget build(BuildContext context) { @@ -27,10 +31,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { } Widget _buildCancelButton(BuildContext context) { - return CancelButton( - onPressed: onCancel, - label: 'Cancel', - ); + return CancelButton(onPressed: onCancel, label: cancelButtonLabel); } Widget _buildSaveButton() { @@ -39,7 +40,7 @@ class SpaceDetailsActionButtons extends StatelessWidget { borderRadius: 10, backgroundColor: ColorsManager.secondaryColor, foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), + child: Text(saveButtonLabel), ); } } From bb846f797f7cad5ca811a6839d4614a5f21adbe0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 6 Jul 2025 16:54:15 +0300 Subject: [PATCH 05/15] Implement Tag Assignment and Device Addition Features: - Introduced AssignTagsDialog for assigning tags to devices, enhancing user interaction and organization. - Added AddDeviceTypeWidget for adding new device types, improving the flexibility of device management. - Created ProductTypeCard and ProductTypeCardCounter for better representation and interaction with device types. - Enhanced AssignTagsTable for displaying and managing product allocations, improving maintainability and user experience. --- .../widgets/space_details_devices_box.dart | 12 +- .../widgets/add_device_type_widget.dart | 87 ++++++++ .../widgets/assign_tags_dialog.dart | 39 ++++ .../widgets/assign_tags_table.dart | 170 ++++++++++++++ .../widgets/product_tag_field.dart | 209 ++++++++++++++++++ .../widgets/product_type_card.dart | 61 +++++ .../widgets/product_type_card_counter.dart | 65 ++++++ 7 files changed, 641 insertions(+), 2 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart 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 fba905dc..3dfd919c 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 @@ -3,6 +3,7 @@ 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/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -63,14 +64,14 @@ class SpaceDetailsDevicesBox extends StatelessWidget { ), ), EditChip( - onTap: () {}, + onTap: () => _showAssignTagsDialog(context), ), ], ), ); } else { return TextButton( - onPressed: () {}, + onPressed: () => _showAssignTagsDialog(context), style: TextButton.styleFrom( padding: EdgeInsets.zero, ), @@ -85,6 +86,13 @@ class SpaceDetailsDevicesBox extends StatelessWidget { } } + void _showAssignTagsDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AssignTagsDialog(space: space), + ); + } + String _getDeviceIcon(String productType) => switch (devicesTypesMap[productType]) { DeviceType.LightBulb => Assets.lightBulb, 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 new file mode 100644 index 00000000..aaef7a91 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -0,0 +1,87 @@ +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/presentation/bloc/products_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.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'; + +class AddDeviceTypeWidget extends StatelessWidget { + const AddDeviceTypeWidget({super.key}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final crossAxisCount = switch (context.screenWidth) { + > 1200 => 8, + > 800 => 5, + _ => 3, + }; + + return BlocProvider( + create: (_) => ProductsBloc(RemoteProductsService(HTTPService())) + ..add(const LoadProducts()), + child: Builder( + builder: (context) => AlertDialog( + title: const Text('Add Devices'), + backgroundColor: ColorsManager.whiteColors, + content: BlocBuilder( + builder: (context, state) { + return switch (state) { + ProductsInitial() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoading() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoaded(:final products) => SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + const SizedBox(height: 16), + 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], + ), + ), + ), + ], + ), + ), + ), + ProductsFailure(:final errorMessage) => Center( + child: Text( + errorMessage, + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + }; + }, + ), + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..706bc0a8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.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_table.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AssignTagsDialog extends StatelessWidget { + const AssignTagsDialog({required this.space, super.key}); + + final SpaceDetailsModel space; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('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), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: () { + showDialog( + context: context, + builder: (context) => const AddDeviceTypeWidget(), + ); + }, + cancelButtonLabel: 'Add New Device', + ), + ], + ); + } +} 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 new file mode 100644 index 00000000..03226b14 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.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/domain/models/tag.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.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 { + const AssignTagsTable({ + required this.productAllocations, + super.key, + }); + + final List productAllocations; + + @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) { + return DataColumn(label: Text(label, style: context.textTheme.bodyMedium)); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), + key: ValueKey(widget.productAllocations.length), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + _buildDataColumn('#'), + _buildDataColumn('Device'), + _buildDataColumn('Tag'), + _buildDataColumn('Location'), + ], + rows: widget.productAllocations.isEmpty + ? [ + DataRow( + cells: [ + DataCell( + Center( + child: Text( + 'No Devices Available', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + ), + ), + DataCell.empty, + DataCell.empty, + DataCell.empty, + ], + ), + ] + : List.generate(widget.productAllocations.length, (index) { + final productAllocation = widget.productAllocations[index]; + final controller = _controllers[index]; + + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + productAllocation.product.name, + overflow: TextOverflow.ellipsis, + )), + const SizedBox(width: 10), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1, + ), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: ColorsManager.lightGreyColor, + size: 16, + ), + onPressed: () { + // TODO: Delete the product allocation + }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + ], + ), + ), + DataCell( + Container( + alignment: Alignment.centerLeft, + width: double.infinity, + child: ProductTagField( + key: ValueKey('dropdown_${const Uuid().v4()}_$index'), + productName: productAllocation.product.uuid, + initialValue: null, + onSelected: (value) { + controller.text = value.name; + }, + items: const [ + Tag( + uuid: '', + name: 'Tag', + createdAt: '', + updatedAt: '', + ), + ], + ), + ), + ), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: const [], + // items: widget.locations, + selectedValue: productAllocation.tag.name.isEmpty + ? 'Main Space' + : productAllocation.tag.name, + onSelected: (value) {}, + )), + ), + ], + ); + }), + ), + ); + } +} 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 new file mode 100644 index 00000000..04c82370 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ProductTagField extends StatefulWidget { + final List items; + final ValueChanged onSelected; + final Tag? initialValue; + final String productName; + + const ProductTagField({ + super.key, + required this.items, + required this.onSelected, + this.initialValue, + required this.productName, + }); + + @override + State createState() => _ProductTagFieldState(); +} + +class _ProductTagFieldState extends State { + bool _isOpen = false; + 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(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + _overlayEntry = null; + _isOpen = false; + super.dispose(); + } + + void _filterItems() => setState(() => _filteredItems = widget.items); + + void _toggleDropdown() { + if (_isOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + } + + void _openDropdown() { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry!); + _isOpen = true; + } + + void _closeDropdown() { + if (_isOpen && _overlayEntry != null) { + _overlayEntry!.remove(); + _overlayEntry = null; + _isOpen = false; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.transparentColor), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + 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(); + }, + style: context.textTheme.bodyMedium, + decoration: const InputDecoration( + hintText: 'Enter or Select a tag', + border: InputBorder.none, + ), + ), + ), + GestureDetector( + onTap: _toggleDropdown, + child: const Icon(Icons.arrow_drop_down), + ), + ], + ), + ), + ); + } + + OverlayEntry _createOverlayEntry() { + final renderBox = context.findRenderObject()! as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (context) { + return GestureDetector( + onTap: _closeDropdown, + behavior: HitTestBehavior.translucent, + child: Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy + size.height, + width: size.width, + child: Material( + elevation: 4.0, + 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: 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(); + }, + ), + ); + }, + ); + }, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart new file mode 100644 index 00000000..203e8502 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ProductTypeCard extends StatelessWidget { + const ProductTypeCard({super.key, required this.product}); + final Product product; + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + color: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: DeviceIconWidget( + icon: product.name, + ), + ), + _buildName(context, product.name), + CounterWidget( + isCreate: false, + initialCount: 0, + onCountChanged: (newCount) {}, + ), + const SizedBox(height: 4), + ], + ), + ), + ); + } + + Widget _buildName(BuildContext context, String name) { + return Expanded( + child: SizedBox( + height: 35, + child: Text( + name, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart new file mode 100644 index 00000000..3f9cb463 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ProductTypeCardCounter extends StatefulWidget { + const ProductTypeCardCounter({ + super.key, + required this.onIncrement, + required this.onDecrement, + required this.count, + }); + + final int count; + + final void Function() onIncrement; + final void Function() onDecrement; + + @override + State createState() => _ProductTypeCardCounterState(); +} + +class _ProductTypeCardCounterState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + _buildCounterButton( + Icons.remove, + widget.onDecrement, + ), + Text( + widget.count.toString(), + style: theme.textTheme.bodyLarge?.copyWith( + color: ColorsManager.spaceColor, + ), + ), + _buildCounterButton(Icons.add, widget.onIncrement), + ], + ), + ); + } + + Widget _buildCounterButton( + IconData icon, + VoidCallback onPressed, + ) { + return GestureDetector( + onTap: onPressed, + child: Icon( + icon, + color: ColorsManager.spaceColor.withValues(alpha: 0.3), + size: 18, + ), + ); + } +} From f0bfe085a4e145a2ade2c9f926a91494a46c4fed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 09:34:11 +0300 Subject: [PATCH 06/15] Enhance Product Model with Icon Mapping: - Added icon mapping functionality to the Product model, allowing dynamic icon retrieval based on product type. - Updated ProductTypeCard to utilize the new icon property, improving UI representation and maintainability. --- .../products/domain/models/product.dart | 37 +++++++++++++++++-- .../widgets/product_type_card.dart | 9 +++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/pages/space_management_v2/modules/products/domain/models/product.dart b/lib/pages/space_management_v2/modules/products/domain/models/product.dart index aac332d6..1a505bc5 100644 --- a/lib/pages/space_management_v2/modules/products/domain/models/product.dart +++ b/lib/pages/space_management_v2/modules/products/domain/models/product.dart @@ -1,15 +1,19 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class Product extends Equatable { - final String uuid; - final String name; - final String productType; const Product({ required this.uuid, required this.name, required this.productType, }); + final String uuid; + final String name; + final String productType; + + String get icon => _mapIconToProduct(productType); + factory Product.fromJson(Map json) { return Product( uuid: json['uuid'] as String? ?? '', @@ -26,6 +30,33 @@ class Product extends Equatable { }; } + static String _mapIconToProduct(String prodType) { + const iconMapping = { + '1G': Assets.Gang1SwitchIcon, + '1GT': Assets.oneTouchSwitch, + '2G': Assets.Gang2SwitchIcon, + '2GT': Assets.twoTouchSwitch, + '3G': Assets.Gang3SwitchIcon, + '3GT': Assets.threeTouchSwitch, + 'CUR': Assets.curtain, + 'CUR_2': Assets.curtain, + 'GD': Assets.garageDoor, + 'GW': Assets.SmartGatewayIcon, + 'DL': Assets.DoorLockIcon, + 'WL': Assets.waterLeakSensor, + 'WH': Assets.waterHeater, + 'WM': Assets.waterLeakSensor, + 'SOS': Assets.sos, + 'AC': Assets.ac, + 'CPS': Assets.presenceSensor, + 'PC': Assets.powerClamp, + 'WPS': Assets.presenceSensor, + 'DS': Assets.doorSensor + }; + + return iconMapping[prodType] ?? Assets.presenceSensor; + } + @override List get props => [uuid, name, productType]; } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart index 203e8502..d364a98a 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -6,7 +6,10 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ProductTypeCard extends StatelessWidget { - const ProductTypeCard({super.key, required this.product}); + const ProductTypeCard({ + required this.product, + super.key, + }); final Product product; @override @@ -18,7 +21,7 @@ class ProductTypeCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Padding( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(4), child: Column( spacing: 8, mainAxisAlignment: MainAxisAlignment.start, @@ -26,7 +29,7 @@ class ProductTypeCard extends StatelessWidget { children: [ Expanded( child: DeviceIconWidget( - icon: product.name, + icon: product.icon, ), ), _buildName(context, product.name), From df87e41d619cacc8aec2b6f02e2e74893de1da6f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:15:33 +0300 Subject: [PATCH 07/15] Refactor AddDeviceTypeWidget and ProductTypeCard Components: - Replaced ProductTypeCard with ProductsGrid in AddDeviceTypeWidget for improved layout and maintainability. - Converted ProductTypeCardCounter from StatefulWidget to StatelessWidget, simplifying its implementation. - Updated ProductTypeCard to accept count and increment/decrement callbacks, enhancing its reusability and interaction. - Introduced ProductsGrid to manage product display in a grid format, improving UI organization and responsiveness. --- .../widgets/add_device_type_widget.dart | 74 +++++-------------- .../widgets/product_type_card.dart | 23 +++--- .../widgets/product_type_card_counter.dart | 19 ++--- .../presentation/widgets/products_grid.dart | 57 ++++++++++++++ 4 files changed, 93 insertions(+), 80 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart 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 aaef7a91..5cfceea8 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 @@ -2,7 +2,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/presentation/bloc/products_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.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'; @@ -12,13 +12,6 @@ class AddDeviceTypeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - final crossAxisCount = switch (context.screenWidth) { - > 1200 => 8, - > 800 => 5, - _ => 3, - }; - return BlocProvider( create: (_) => ProductsBloc(RemoteProductsService(HTTPService())) ..add(const LoadProducts()), @@ -27,57 +20,24 @@ class AddDeviceTypeWidget extends StatelessWidget { title: const Text('Add Devices'), backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( - builder: (context, state) { - return switch (state) { - ProductsInitial() => const Center( - child: CircularProgressIndicator(), - ), - ProductsLoading() => const Center( - child: CircularProgressIndicator(), - ), - ProductsLoaded(:final products) => SingleChildScrollView( - child: Container( - width: size.width * 0.9, - height: size.height * 0.65, - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - const SizedBox(height: 16), - 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], - ), - ), - ), - ], - ), + builder: (context, state) => switch (state) { + ProductsInitial() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoading() => const Center( + child: CircularProgressIndicator(), + ), + ProductsLoaded(:final products) => ProductsGrid( + products: products, + ), + ProductsFailure(:final errorMessage) => Center( + child: Text( + errorMessage, + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, ), ), - ProductsFailure(:final errorMessage) => Center( - child: Text( - errorMessage, - style: context.textTheme.bodyMedium?.copyWith( - color: context.theme.colorScheme.error, - ), - ), - ), - }; + ), }, ), ), diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart index d364a98a..c3910aca 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart'; import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -8,9 +8,16 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ProductTypeCard extends StatelessWidget { const ProductTypeCard({ required this.product, + required this.count, + required this.onIncrement, + required this.onDecrement, super.key, }); + final Product product; + final int count; + final void Function() onIncrement; + final void Function() onDecrement; @override Widget build(BuildContext context) { @@ -27,16 +34,12 @@ class ProductTypeCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: DeviceIconWidget( - icon: product.icon, - ), - ), + Expanded(child: DeviceIconWidget(icon: product.icon)), _buildName(context, product.name), - CounterWidget( - isCreate: false, - initialCount: 0, - onCountChanged: (newCount) {}, + ProductTypeCardCounter( + onIncrement: onIncrement, + onDecrement: onDecrement, + count: count, ), const SizedBox(height: 4), ], diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart index 3f9cb463..605fde2f 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class ProductTypeCardCounter extends StatefulWidget { +class ProductTypeCardCounter extends StatelessWidget { const ProductTypeCardCounter({ super.key, required this.onIncrement, @@ -10,19 +11,11 @@ class ProductTypeCardCounter extends StatefulWidget { }); final int count; - final void Function() onIncrement; final void Function() onDecrement; - @override - State createState() => _ProductTypeCardCounterState(); -} - -class _ProductTypeCardCounterState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( @@ -35,15 +28,15 @@ class _ProductTypeCardCounterState extends State { children: [ _buildCounterButton( Icons.remove, - widget.onDecrement, + onDecrement, ), Text( - widget.count.toString(), - style: theme.textTheme.bodyLarge?.copyWith( + count.toString(), + style: context.textTheme.bodyLarge?.copyWith( color: ColorsManager.spaceColor, ), ), - _buildCounterButton(Icons.add, widget.onIncrement), + _buildCounterButton(Icons.add, onIncrement), ], ), ); 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 new file mode 100644 index 00000000..b01fb10f --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart @@ -0,0 +1,57 @@ +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/tags/presentation/widgets/product_type_card.dart'; +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}); + + final List products; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final crossAxisCount = switch (context.screenWidth) { + > 1200 => 8, + > 800 => 5, + _ => 3, + }; + return SingleChildScrollView( + child: Container( + width: size.width * 0.9, + height: size.height * 0.65, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + const SizedBox(height: 16), + 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: () {}, + ), + ), + ), + ], + ), + ), + ); + } +} From 47bd6ff89ed3f07e10236bb9d72c0ded7f1be9de Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:20:51 +0300 Subject: [PATCH 08/15] Refactor AddDeviceTypeWidget for Improved Error Handling and Loading States: - Extracted loading and error handling logic into separate methods for better readability and maintainability. - Updated UI to utilize centralized loading and failure widgets, enhancing user experience during data fetching. --- .../widgets/add_device_type_widget.dart | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) 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 5cfceea8..fa2af925 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 @@ -21,22 +21,14 @@ class AddDeviceTypeWidget extends StatelessWidget { backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( builder: (context, state) => switch (state) { - ProductsInitial() => const Center( - child: CircularProgressIndicator(), - ), - ProductsLoading() => const Center( - child: CircularProgressIndicator(), - ), + ProductsInitial() => _buildLoading(context), + ProductsLoading() => _buildLoading(context), ProductsLoaded(:final products) => ProductsGrid( products: products, ), - ProductsFailure(:final errorMessage) => Center( - child: Text( - errorMessage, - style: context.textTheme.bodyMedium?.copyWith( - color: context.theme.colorScheme.error, - ), - ), + ProductsFailure(:final errorMessage) => _buildFailure( + context, + errorMessage, ), }, ), @@ -44,4 +36,25 @@ class AddDeviceTypeWidget extends StatelessWidget { ), ); } + + Widget _buildLoading(BuildContext context) => SizedBox( + width: context.screenWidth * 0.9, + height: context.screenHeight * 0.65, + child: const Center(child: CircularProgressIndicator()), + ); + + Widget _buildFailure(BuildContext context, String errorMessage) { + return SizedBox( + width: context.screenWidth * 0.9, + height: context.screenHeight * 0.65, + child: Center( + child: SelectableText( + errorMessage, + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + ); + } } From e917225c3d1d170f4c5a7d24783ae38f638adea3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:36:42 +0300 Subject: [PATCH 09/15] Refactor Widgets for Improved UI Consistency and Usability: - Replaced Text with SelectableText in AddDeviceTypeWidget and AssignTagsTable for better text selection and accessibility. - Simplified onCancel action in AssignTagsDialog for improved readability. - Enhanced ProductsGrid layout by removing unnecessary Column widget, streamlining the widget structure for better performance and maintainability. --- .../widgets/add_device_type_widget.dart | 2 +- .../widgets/assign_tags_dialog.dart | 10 ++--- .../widgets/assign_tags_table.dart | 5 ++- .../presentation/widgets/products_grid.dart | 43 ++++++++----------- 4 files changed, 27 insertions(+), 33 deletions(-) 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 fa2af925..673efe7e 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 @@ -17,7 +17,7 @@ class AddDeviceTypeWidget extends StatelessWidget { ..add(const LoadProducts()), child: Builder( builder: (context) => AlertDialog( - title: const Text('Add Devices'), + title: const SelectableText('Add Devices'), backgroundColor: ColorsManager.whiteColors, content: BlocBuilder( builder: (context, state) => switch (state) { 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 706bc0a8..25f08d30 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 @@ -25,12 +25,10 @@ class AssignTagsDialog extends StatelessWidget { actions: [ SpaceDetailsActionButtons( onSave: () {}, - onCancel: () { - showDialog( - context: context, - builder: (context) => const AddDeviceTypeWidget(), - ); - }, + onCancel: () => showDialog( + context: context, + builder: (context) => const AddDeviceTypeWidget(), + ), cancelButtonLabel: 'Add New Device', ), ], 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 03226b14..196b4102 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 @@ -42,7 +42,8 @@ class _AssignTagsTableState extends State { } DataColumn _buildDataColumn(String label) { - return DataColumn(label: Text(label, style: context.textTheme.bodyMedium)); + return DataColumn( + label: SelectableText(label, style: context.textTheme.bodyMedium)); } @override @@ -69,7 +70,7 @@ class _AssignTagsTableState extends State { cells: [ DataCell( Center( - child: Text( + child: SelectableText( 'No Devices Available', style: context.textTheme.bodyMedium?.copyWith( color: ColorsManager.lightGrayColor, 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 b01fb10f..7fe5ec26 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 @@ -25,31 +25,26 @@ class ProductsGrid extends StatelessWidget { color: ColorsManager.textFieldGreyColor, borderRadius: BorderRadius.circular(8), ), - child: Column( - children: [ - const SizedBox(height: 16), - 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: 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: () {}, + ), + ), ), ), ); From e523a8391225bbfe3645d3c5846f895868dd920d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 10:50:03 +0300 Subject: [PATCH 10/15] Refactor Tags Service and Bloc for Improved Data Handling: - Updated RemoteTagsService to remove LoadTagsParam and fetch project UUID internally, enhancing encapsulation and reducing parameter dependency. - Modified TagsService interface to reflect the new loading method signature. - Adjusted TagsBloc to align with the updated service method, simplifying the loading process. - Enhanced AssignTagsTable and AddDeviceTypeWidget to utilize the new data flow, improving maintainability and user experience. --- .../data/services/remote_tags_service.dart | 22 +- .../tags/domain/services/tags_service.dart | 3 +- .../tags/presentation/bloc/tags_bloc.dart | 3 +- .../tags/presentation/bloc/tags_event.dart | 7 +- .../widgets/add_device_type_widget.dart | 8 + .../widgets/assign_tags_table.dart | 246 ++++++++++-------- 6 files changed, 154 insertions(+), 135 deletions(-) diff --git a/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart b/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart index b5545bd3..76ceec71 100644 --- a/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart +++ b/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart @@ -1,10 +1,9 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.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/domain/params/load_tags_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; -import 'package:syncrow_web/utils/constants/api_const.dart'; final class RemoteTagsService implements TagsService { const RemoteTagsService(this._httpService); @@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService { static const _defaultErrorMessage = 'Failed to load tags'; @override - Future> loadTags(LoadTagsParam param) async { - if (param.projectUuid == null) { - throw Exception('Project UUID is required'); - } - + Future> loadTags() async { try { final response = await _httpService.get( - path: ApiEndpoints.listTags.replaceAll( - '{projectUuid}', - param.projectUuid!, - ), + path: await _makeUrl(), expectedResponseModel: (json) { final result = json as Map; final data = result['data'] as List; @@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is required'); + } + return '/projects/$projectUuid/tags'; + } } diff --git a/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart b/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart index ae097020..cf36527a 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart @@ -1,6 +1,5 @@ import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart'; abstract interface class TagsService { - Future> loadTags(LoadTagsParam param); + Future> loadTags(); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart index e51884cb..b81fcb76 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.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/domain/params/load_tags_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -21,7 +20,7 @@ class TagsBloc extends Bloc { ) async { emit(TagsLoading()); try { - final tags = await _tagsService.loadTags(event.param); + final tags = await _tagsService.loadTags(); emit(TagsLoaded(tags)); } on APIException catch (e) { emit(TagsFailure(e.message)); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart index 99134cab..8965b7b0 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart @@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable { } class LoadTags extends TagsEvent { - final LoadTagsParam param; - - const LoadTags(this.param); - - @override - List get props => [param]; + const LoadTags(); } 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 673efe7e..f77f10b5 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 @@ -2,6 +2,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/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'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -32,6 +33,13 @@ class AddDeviceTypeWidget extends StatelessWidget { ), }, ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: Navigator.of(context).pop, + saveButtonLabel: 'Next', + ), + ], ), ), ); 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 196b4102..3d92109d 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 @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +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/domain/models/tag.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/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'; @@ -48,123 +51,138 @@ class _AssignTagsTableState extends State { @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(20), - child: DataTable( - headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), - key: ValueKey(widget.productAllocations.length), - border: TableBorder.all( - color: ColorsManager.dataHeaderGrey, - width: 1, - borderRadius: BorderRadius.circular(20), - ), - columns: [ - _buildDataColumn('#'), - _buildDataColumn('Device'), - _buildDataColumn('Tag'), - _buildDataColumn('Location'), - ], - rows: widget.productAllocations.isEmpty - ? [ - DataRow( - cells: [ - DataCell( - Center( - child: SelectableText( - 'No Devices Available', - style: context.textTheme.bodyMedium?.copyWith( - color: ColorsManager.lightGrayColor, - ), - ), - ), - ), - DataCell.empty, - DataCell.empty, - DataCell.empty, + return BlocProvider( + create: (BuildContext context) => TagsBloc( + RemoteTagsService(HTTPService()), + )..add(const LoadTags()), + child: BlocBuilder( + builder: (context, state) { + return switch (state) { + TagsLoading() || TagsInitial() => const Center( + child: CircularProgressIndicator(), + ), + TagsFailure(:final message) => Center( + child: Text(message), + ), + TagsLoaded(:final tags) => ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + ColorsManager.dataHeaderGrey, + ), + key: ValueKey(widget.productAllocations.length), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + _buildDataColumn('#'), + _buildDataColumn('Device'), + _buildDataColumn('Tag'), + _buildDataColumn('Location'), ], - ), - ] - : List.generate(widget.productAllocations.length, (index) { - final productAllocation = widget.productAllocations[index]; - final controller = _controllers[index]; + rows: widget.productAllocations.isEmpty + ? [ + DataRow( + cells: [ + DataCell( + Center( + child: SelectableText( + 'No Devices Available', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + ), + ), + DataCell.empty, + DataCell.empty, + DataCell.empty, + ], + ), + ] + : List.generate(widget.productAllocations.length, (index) { + final productAllocation = widget.productAllocations[index]; + final controller = _controllers[index]; - return DataRow( - cells: [ - DataCell(Text((index + 1).toString())), - DataCell( - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - productAllocation.product.name, - overflow: TextOverflow.ellipsis, - )), - const SizedBox(width: 10), - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1, + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + productAllocation.product.name, + overflow: TextOverflow.ellipsis, + )), + const SizedBox(width: 10), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1, + ), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: ColorsManager.lightGreyColor, + size: 16, + ), + onPressed: () { + // TODO: Delete the product allocation + }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + ], + ), ), - ), - child: IconButton( - icon: const Icon( - Icons.close, - color: ColorsManager.lightGreyColor, - size: 16, + DataCell( + Container( + alignment: Alignment.centerLeft, + width: double.infinity, + child: ProductTagField( + key: ValueKey( + 'dropdown_${const Uuid().v4()}_$index'), + productName: productAllocation.product.uuid, + initialValue: null, + onSelected: (value) { + controller.text = value.name; + }, + items: tags, + ), + ), ), - onPressed: () { - // TODO: Delete the product allocation - }, - tooltip: 'Delete Tag', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ), - ], - ), - ), - DataCell( - Container( - alignment: Alignment.centerLeft, - width: double.infinity, - child: ProductTagField( - key: ValueKey('dropdown_${const Uuid().v4()}_$index'), - productName: productAllocation.product.uuid, - initialValue: null, - onSelected: (value) { - controller.text = value.name; - }, - items: const [ - Tag( - uuid: '', - name: 'Tag', - createdAt: '', - updatedAt: '', - ), - ], - ), - ), - ), - DataCell( - SizedBox( - width: double.infinity, - child: DialogDropdown( - items: const [], - // items: widget.locations, - selectedValue: productAllocation.tag.name.isEmpty - ? 'Main Space' - : productAllocation.tag.name, - onSelected: (value) {}, - )), - ), - ], - ); - }), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: const [], + // items: widget.locations, + selectedValue: + productAllocation.tag.name.isEmpty + ? 'Main Space' + : productAllocation.tag.name, + onSelected: (value) {}, + )), + ), + ], + ); + }), + ), + ), + _ => const SizedBox.shrink(), + }; + }, ), ); } From dc7064d142750e027c0aaa6618fd7fefda0429be Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:25:37 +0300 Subject: [PATCH 11/15] Add Factory Method for Empty Tag Instance in Tag Model. --- .../modules/tags/domain/models/tag.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart index 1044d888..370bdf47 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart @@ -13,6 +13,13 @@ class Tag extends Equatable { required this.updatedAt, }); + factory Tag.empty() => const Tag( + uuid: '', + name: '', + createdAt: '', + updatedAt: '', + ); + factory Tag.fromJson(Map json) { return Tag( uuid: json['uuid'] as String, From 7f0484eec6bb6c5cb351a5ee3f555f014d8409ce Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:18 +0300 Subject: [PATCH 12/15] Add UpdateSpaceDetails Event. --- .../space_details_model_bloc.dart | 8 ++++++++ .../space_details_model_event.dart | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart index 21a72557..15a22fda 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -13,6 +13,7 @@ class SpaceDetailsModelBloc extends Bloc(_onUpdateSpaceDetailsSubspaces); on( _onUpdateSpaceDetailsProductAllocations); + on(_onUpdateSpaceDetails); } void _onUpdateSpaceDetailsIcon( @@ -42,4 +43,11 @@ class SpaceDetailsModelBloc extends Bloc emit, + ) { + emit(event.space); + } } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart index d3e04bb9..abf9cd98 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart @@ -42,3 +42,12 @@ final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent @override List get props => [productAllocations]; } + +final class UpdateSpaceDetails extends SpaceDetailsModelEvent { + const UpdateSpaceDetails(this.space); + + final SpaceDetailsModel space; + + @override + List get props => [space]; +} From ddfd4ee153506f0b111f1d4ad77819ac592dfd92 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:39 +0300 Subject: [PATCH 13/15] removed print statement. --- .../presentation/helpers/space_details_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index 6b95556a..de2c40f0 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -18,7 +18,7 @@ abstract final class SpaceDetailsDialogHelper { context: context, title: const SelectableText('Create Space'), spaceModel: SpaceModel.empty(), - onSave: print, + onSave: (space) {}, ), ), ); From 31019602016eb76ec1dd65fca3ed02e15fc19b44 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:26:59 +0300 Subject: [PATCH 14/15] 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), + ); + }, ), ), ); From 4c0647946963313722eb36c5558a8ba72cd52609 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 7 Jul 2025 14:54:25 +0300 Subject: [PATCH 15/15] Replaced Column with ListView in SpaceDetailsForm to enhance scrolling behavior and accommodate dynamic content. --- .../presentation/widgets/space_details_form.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart index d0495dd3..e4007511 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -42,9 +42,8 @@ class SpaceDetailsForm extends StatelessWidget { Expanded(child: SpaceIconPicker(iconPath: space.icon)), Expanded( flex: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + child: ListView( + shrinkWrap: true, children: [ SpaceNameTextField( initialValue: space.spaceName, @@ -52,7 +51,7 @@ class SpaceDetailsForm extends StatelessWidget { (subspace) => subspace.name == value, ), ), - const Spacer(), + const SizedBox(height: 32), SpaceSubSpacesBox( subspaces: space.subspaces, ),