diff --git a/lib/pages/spaces_management/all_spaces/assign_tag_models/views/assign_tag_models_dialog.dart b/lib/pages/spaces_management/all_spaces/assign_tag_models/views/assign_tag_models_dialog.dart deleted file mode 100644 index b04e1c35..00000000 --- a/lib/pages/spaces_management/all_spaces/assign_tag_models/views/assign_tag_models_dialog.dart +++ /dev/null @@ -1,297 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; -import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; -import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class AssignTagModelsDialog extends StatefulWidget { - final List? products; - final List? subspaces; - final List? initialTags; - final ValueChanged>? onTagsAssigned; - final List addedProducts; - final List? allTags; - - const AssignTagModelsDialog( - {Key? key, - required this.products, - required this.subspaces, - required this.addedProducts, - this.initialTags, - this.onTagsAssigned, - this.allTags}) - : super(key: key); - - @override - AssignTagModelsDialogState createState() => AssignTagModelsDialogState(); -} - -class AssignTagModelsDialogState extends State { - late List tags; - late List selectedProducts; - late List locations; - late List otherTags; - late List controllers; - Set usedTags = {}; - String? errorMessage; - bool isSaveEnabled = true; - - @override - void initState() { - super.initState(); - - // Initialize tags from widget.initialTags or create new ones if it's empty - tags = widget.initialTags?.isNotEmpty == true - ? widget.initialTags! - : widget.addedProducts - .expand((selectedProduct) => List.generate( - selectedProduct.count, // Generate `count` number of tags - (index) => TagModel( - tag: '', // Initialize each tag with a default value - product: selectedProduct.product, - location: 'None', // Default location - ), - )) - .toList(); - - // Initialize selected products - selectedProducts = widget.addedProducts; - - // Initialize locations from subspaces or empty list if null - locations = widget.subspaces != null - ? widget.subspaces!.map((subspace) => subspace.subspaceName).toList() - : []; - locations.add("None"); - - otherTags = widget.allTags != null ? widget.allTags! : []; - - controllers = List.generate( - tags.length, - (index) => TextEditingController(text: tags[index].tag), - ); - - for (final tag in tags) { - if (tag.tag != null && tag.tag!.isNotEmpty) { - usedTags.add(tag.tag!); - } - } - - _validateTags(); - } - - void _validateTags() { - // Disable save if any tag is empty - final hasEmptyTag = - tags.any((tag) => tag.tag == null || tag.tag!.trim().isEmpty); - setState(() { - isSaveEnabled = !hasEmptyTag; - }); - } - - @override - void dispose() { - // Dispose of controllers - for (final controller in controllers) { - controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Assign Tags'), - backgroundColor: ColorsManager.whiteColors, - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.4, - decoration: BoxDecoration( - border: - Border.all(color: ColorsManager.dataHeaderGrey, width: 1), - borderRadius: BorderRadius.circular(20), - ), - child: DataTable( - border: TableBorder.all( - color: ColorsManager.dataHeaderGrey, - width: 1, - borderRadius: BorderRadius.circular(20), - ), - columns: const [ - DataColumn(label: Text('#')), - DataColumn(label: Text('Device')), - DataColumn(label: Text('Tag')), - DataColumn(label: Text('Location')), - ], - rows: List.generate(tags.length, (index) { - final tag = tags[index]; - final controller = controllers[index]; - - return DataRow( - cells: [ - DataCell(Center(child: Text(index.toString()))), - DataCell( - Center(child: Text(tag.product?.name ?? 'Unknown'))), - DataCell( - Row( - children: [ - Expanded( - child: TextFormField( - controller: controller, - decoration: const InputDecoration( - hintText: 'Enter Tag', - border: InputBorder.none, - hintStyle: TextStyle( - color: Colors.grey, - fontSize: 14, - ), - ), - style: const TextStyle( - color: Colors.black, - fontSize: 14, - ), - onChanged: (value) { - setState(() { - tag.tag = value.trim(); - _validateTags(); - }); - }, - ), - ), - PopupMenuButton( - icon: const Icon(Icons.arrow_drop_down, - color: Colors.black), - onSelected: (value) { - setState(() { - if (tag.tag != null && tag.tag!.isNotEmpty) { - usedTags.remove(tag.tag!); - } - controller.text = value; - tag.tag = value; - - if (tag.tag != null && tag.tag!.isNotEmpty) { - usedTags.add(tag.tag!); - } - _validateTags(); // Validate after selection - }); - }, - color: Colors.white, - itemBuilder: (context) { - return widget.allTags! - .where((tagValue) => - !usedTags.contains(tagValue)) - .map((tagValue) { - return PopupMenuItem( - value: tagValue, - child: Text( - tagValue, - style: const TextStyle( - color: Color(0xFF5D5D5D), - fontSize: 14, - ), - ), - ); - }).toList(); - }, - ), - ], - ), - ), - DataCell( - Center( - child: DropdownButtonHideUnderline( - child: DropdownButton( - alignment: AlignmentDirectional.center, - value: locations.contains(tag.location) - ? tag.location - : null, - onChanged: (value) { - setState(() { - tag.location = value ?? 'None'; - }); - }, - dropdownColor: ColorsManager.whiteColors, - icon: const Icon(Icons.arrow_drop_down, - color: Colors.black), - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - isExpanded: true, - items: locations - .map((location) => DropdownMenuItem( - value: location, - child: Text( - location, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Color(0xFF5D5D5D), - ), - ), - )) - .toList(), - ), - ), - ), - ), - ], - ); - }), - ), - ), - if (errorMessage != null) - Container( - width: MediaQuery.of(context).size.width * 0.4, - padding: const EdgeInsets.only(top: 8.0, left: 12.0), - child: Text( - errorMessage!, - style: const TextStyle( - color: Colors.red, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Back'), - style: ElevatedButton.styleFrom( - backgroundColor: ColorsManager.boxColor, - foregroundColor: ColorsManager.blackColor, - ), - ), - ElevatedButton( - onPressed: isSaveEnabled - ? () { - Navigator.of(context).pop(); - if (widget.onTagsAssigned != null) { - widget.onTagsAssigned!(tags); - } - } - : null, // Disable the button if validation fails - child: const Text('Save'), - style: ElevatedButton.styleFrom( - backgroundColor: isSaveEnabled - ? ColorsManager.secondaryColor - : Colors.grey, // Change button color when disabled - foregroundColor: ColorsManager.whiteColors, - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart new file mode 100644 index 00000000..d51badca --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart @@ -0,0 +1,125 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +class AssignTagModelBloc + extends Bloc { + AssignTagModelBloc() : super(AssignTagModelInitial()) { + on((event, emit) { + final tags = event.initialTags?.isNotEmpty == true + ? event.initialTags! + : event.addedProducts + .expand((selectedProduct) => List.generate( + selectedProduct.count, + (index) => TagModel( + tag: '', + product: selectedProduct.product, + location: 'None', + ), + )) + .toList(); + + emit( + AssignTagModelLoaded(tags: tags, isSaveEnabled: _validateTags(tags))); + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + tags[event.index].tag = event.tag; + emit(AssignTagModelLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + errorMessage: _getValidationError(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + + // Use copyWith for immutability + tags[event.index] = + tags[event.index].copyWith(location: event.location); + + emit(AssignTagModelLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags); + + emit(AssignTagModelLoaded( + tags: tags, + isSaveEnabled: _validateTags(tags), + errorMessage: _getValidationError(tags), + )); + } + }); + + on((event, emit) { + final currentState = state; + + if (currentState is AssignTagModelLoaded && + currentState.tags.isNotEmpty) { + final updatedTags = List.from(currentState.tags) + ..remove(event.tagToDelete); + + emit(AssignTagModelLoaded( + tags: updatedTags, + isSaveEnabled: _validateTags(updatedTags), + )); + } else { + emit(const AssignTagModelLoaded( + tags: [], + isSaveEnabled: false, + )); + } + }); + } + + bool _validateTags(List tags) { + if (tags.isEmpty) { + return false; + } + final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet(); + final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); + return uniqueTags.length == tags.length && !hasEmptyTag; + } + + String? _getValidationError(List tags) { + final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); + if (hasEmptyTag) return 'Tags cannot be empty.'; + final duplicateTags = tags + .map((tag) => tag.tag?.trim() ?? '') + .fold>({}, (map, tag) { + map[tag] = (map[tag] ?? 0) + 1; + return map; + }) + .entries + .where((entry) => entry.value > 1) + .map((entry) => entry.key) + .toList(); + + if (duplicateTags.isNotEmpty) { + return 'Duplicate tags found: ${duplicateTags.join(', ')}'; + } + + return null; + } +} diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart new file mode 100644 index 00000000..75c9ddc1 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; + +abstract class AssignTagModelEvent extends Equatable { + const AssignTagModelEvent(); + + @override + List get props => []; +} + +class InitializeTagModels extends AssignTagModelEvent { + final List? initialTags; + final List addedProducts; + + const InitializeTagModels({ + required this.initialTags, + required this.addedProducts, + }); + + @override + List get props => [initialTags ?? [], addedProducts]; +} + +class UpdateTagModel extends AssignTagModelEvent { + final int index; + final String tag; + + const UpdateTagModel({required this.index, required this.tag}); + + @override + List get props => [index, tag]; +} + +class UpdateLocation extends AssignTagModelEvent { + final int index; + final String location; + + const UpdateLocation({required this.index, required this.location}); + + @override + List get props => [index, location]; +} + +class ValidateTagModels extends AssignTagModelEvent {} + +class DeleteTagModel extends AssignTagModelEvent { + final TagModel tagToDelete; + final List tags; + + const DeleteTagModel({required this.tagToDelete, required this.tags}); + + @override + List get props => [tagToDelete, tags]; +} diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart new file mode 100644 index 00000000..9812a293 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; + +abstract class AssignTagModelState extends Equatable { + const AssignTagModelState(); + + @override + List get props => []; +} + +class AssignTagModelInitial extends AssignTagModelState {} + +class AssignTagModelLoading extends AssignTagModelState {} + +class AssignTagModelLoaded extends AssignTagModelState { + final List tags; + final bool isSaveEnabled; + final String? errorMessage; + + const AssignTagModelLoaded({ + required this.tags, + required this.isSaveEnabled, + this.errorMessage, + }); + + @override + List get props => [tags, isSaveEnabled]; +} + +class AssignTagModelError extends AssignTagModelState { + final String errorMessage; + + const AssignTagModelError(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart new file mode 100644 index 00000000..e58d75f0 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AssignTagModelsDialog extends StatelessWidget { + final List? products; + final List? subspaces; + final List? initialTags; + final ValueChanged>? onTagsAssigned; + final List addedProducts; + final List? allTags; + + const AssignTagModelsDialog({ + Key? key, + required this.products, + required this.subspaces, + required this.addedProducts, + this.initialTags, + this.onTagsAssigned, + this.allTags, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final List locations = + (subspaces ?? []).map((subspace) => subspace.subspaceName).toList(); + return BlocProvider( + create: (_) => AssignTagModelBloc() + ..add(InitializeTagModels( + initialTags: initialTags, + addedProducts: addedProducts, + )), + child: BlocBuilder( + builder: (context, state) { + if (state is AssignTagModelLoaded) { + print( + "Rebuilding UI with updated locations: ${state.tags.map((e) => e.location)}"); + + final controllers = List.generate( + state.tags.length, + (index) => TextEditingController(text: state.tags[index].tag), + ); + + return AlertDialog( + title: const Text('Assign Tags'), + backgroundColor: ColorsManager.whiteColors, + content: SingleChildScrollView( + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + ColorsManager.dataHeaderGrey), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + DataColumn( + label: Text('#', + style: + Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Device', + style: + Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Tag', + style: + Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Location', + style: + Theme.of(context).textTheme.bodyMedium)), + ], + rows: state.tags.isEmpty + ? [ + const DataRow(cells: [ + DataCell( + Center( + child: Text( + 'No Data Available', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ), + ), + DataCell(SizedBox()), + DataCell(SizedBox()), + DataCell(SizedBox()), + ]) + ] + : List.generate(state.tags.length, (index) { + final tag = state.tags[index]; + final controller = controllers[index]; + + return DataRow( + cells: [ + DataCell(Text(index.toString())), + DataCell( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + tag.product?.name ?? 'Unknown', + overflow: TextOverflow.ellipsis, + )), + IconButton( + icon: const Icon(Icons.close, + color: ColorsManager.warningRed, + size: 16), + onPressed: () { + context + .read() + .add(DeleteTagModel( + tagToDelete: tag, + tags: state.tags)); + }, + tooltip: 'Delete Tag', + ) + ], + ), + ), + DataCell( + Row( + children: [ + Expanded( + child: TextFormField( + controller: controller, + onChanged: (value) { + context + .read() + .add(UpdateTagModel( + index: index, + tag: value.trim(), + )); + }, + decoration: const InputDecoration( + hintText: 'Enter Tag', + border: InputBorder.none, + ), + style: const TextStyle( + fontSize: 14, + color: Colors.black, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + 0.15, + child: PopupMenuButton( + color: ColorsManager.whiteColors, + icon: const Icon( + Icons.arrow_drop_down, + color: Colors.black), + onSelected: (value) { + controller.text = value; + context + .read() + .add(UpdateTagModel( + index: index, + tag: value, + )); + }, + itemBuilder: (context) { + return (allTags ?? []) + .where((tagValue) => !state + .tags + .map((e) => e.tag) + .contains(tagValue)) + .map((tagValue) { + return PopupMenuItem( + textStyle: const TextStyle( + color: ColorsManager + .textPrimaryColor), + value: tagValue, + child: ConstrainedBox( + constraints: + BoxConstraints( + minWidth: MediaQuery.of( + context) + .size + .width * + 0.15, + maxWidth: MediaQuery.of( + context) + .size + .width * + 0.15, + ), + child: Text( + tagValue, + overflow: TextOverflow + .ellipsis, + ), + )); + }).toList(); + }, + ), + ), + ], + ), + ), + DataCell( + DropdownButtonHideUnderline( + child: DropdownButton( + value: tag.location ?? 'None', + dropdownColor: ColorsManager + .whiteColors, // Dropdown background + style: const TextStyle( + color: Colors + .black), // Style for selected text + items: [ + const DropdownMenuItem( + value: 'None', + child: Text( + 'None', + style: TextStyle( + color: ColorsManager + .textPrimaryColor), + ), + ), + ...locations.map((location) { + return DropdownMenuItem( + value: location, + child: Text( + location, + style: const TextStyle( + color: ColorsManager + .textPrimaryColor), + ), + ); + }).toList(), + ], + onChanged: (value) { + if (value != null) { + context + .read() + .add(UpdateLocation( + index: index, + location: value, + )); + } + }, + ), + ), + ), + ], + ); + }), + ), + ), + if (state.errorMessage != null) + Text( + state.errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ], + ), + ), + actions: [ + ElevatedButton( + onPressed: state.isSaveEnabled + ? () { + if (onTagsAssigned != null) { + onTagsAssigned!(state.tags); + } + Navigator.pop(context); + } + : null, + child: const Text('Save'), + ), + ], + ); + } else if (state is AssignTagModelLoading) { + return const Center(child: CircularProgressIndicator()); + } else { + return const Center(child: Text('Something went wrong.')); + } + }, + ), + ); + } +} diff --git a/lib/pages/spaces_management/space_model/models/tag_model.dart b/lib/pages/spaces_management/space_model/models/tag_model.dart index 1e7c594b..1bd2dd02 100644 --- a/lib/pages/spaces_management/space_model/models/tag_model.dart +++ b/lib/pages/spaces_management/space_model/models/tag_model.dart @@ -30,6 +30,18 @@ class TagModel { ); } + TagModel copyWith({ + String? tag, + ProductModel? product, + String? location, + }) { + return TagModel( + tag: tag ?? this.tag, + product: product ?? this.product, + location: location ?? this.location, + ); + } + Map toJson() { return { 'uuid': uuid, diff --git a/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart b/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart index 3f74ebb8..c6ee5424 100644 --- a/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart +++ b/lib/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/assign_tag_models/views/assign_tag_models_dialog.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; @@ -74,6 +74,8 @@ class AddDeviceTypeModelWidget extends StatelessWidget { onPressed: () async { final currentState = context.read().state; + Navigator.of(context).pop(); + if (currentState.isNotEmpty) { await showDialog( barrierDismissible: false,