From 63353af38bd5885f8a98bb2596e243a95770b964 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 2 Jul 2025 15:03:23 +0300 Subject: [PATCH] Add SpaceDetails dialog and related widgets for creating and editing spaces, including SpaceDetailsDevicesBox and SpaceSubSpacesBox for managing devices and subspaces. --- .../helpers/space_details_dialog_helper.dart | 28 +++- .../widgets/space_details_action_buttons.dart | 14 +- .../widgets/space_details_devices_box.dart | 90 +++++++++++ .../widgets/space_details_dialog.dart | 72 ++++++++- .../widgets/space_name_text_field.dart | 84 ++++++++++ .../widgets/space_sub_spaces_box.dart | 67 ++++++++ .../widgets/space_sub_spaces_dialog.dart | 143 ++++++++++++++++++ .../presentation/widgets/subspace_chip.dart | 55 +++++++ .../widgets/subspace_name_display_widget.dart | 73 +++++++++ 9 files changed, 614 insertions(+), 12 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart 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 e871f4d0..5d6ffee7 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 @@ -1,11 +1,37 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.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_dialog.dart'; abstract final class SpaceDetailsDialogHelper { static void showCreate(BuildContext context) { showDialog( context: context, - builder: (context) => const SpaceDetailsDialog(), + builder: (_) => SpaceDetailsDialog( + title: const Text('Create Space'), + space: SpaceDetailsModel.empty(), + onSave: (space) {}, + ), + ); + } + + static void showEdit( + BuildContext context, { + required SpaceModel spaceModel, + }) { + showDialog( + context: context, + builder: (_) => SpaceDetailsDialog( + title: const Text('Edit Space'), + space: SpaceDetailsModel( + uuid: spaceModel.uuid, + spaceName: spaceModel.spaceName, + icon: spaceModel.icon, + productAllocations: const [], + subspaces: const [], + ), + onSave: (space) {}, + ), ); } } 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 b26a6590..502ae4bb 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 @@ -33,14 +33,12 @@ class SpaceDetailsActionButtons extends StatelessWidget { } Widget _buildSaveButton() { - return Expanded( - child: DefaultButton( - onPressed: onSave, - borderRadius: 10, - backgroundColor: ColorsManager.secondaryColor, - foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), - ), + return DefaultButton( + onPressed: onSave, + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), ); } } 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 new file mode 100644 index 00000000..4873dc09 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.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/extension/build_context_x.dart'; + +class SpaceDetailsDevicesBox extends StatelessWidget { + const SpaceDetailsDevicesBox({super.key, required this.space}); + + final SpaceDetailsModel space; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (space.productAllocations.isNotEmpty || + space.subspaces + .any((subspace) => subspace.productAllocations.isNotEmpty)) + SizedBox( + width: context.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + 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}', // Show count + // 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, + // ), + // ), + // ), + // ), + + EditChip( + onTap: () {}, + ), + ], + ), + ), + ) + else + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Add Devices', + // disabled: isTagsAndSubspaceModelDisabled, + ), + ) + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 7213c99e..43b52ef1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,12 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart' + show SpaceDetailsModel; +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/space_details/presentation/widgets/space_details_devices_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceDetailsDialog extends StatelessWidget { - const SpaceDetailsDialog({super.key}); + const SpaceDetailsDialog({ + required this.title, + required this.space, + required this.onSave, + super.key, + }); + + final Widget title; + final SpaceDetailsModel space; + final void Function(SpaceDetailsModel space) onSave; @override Widget build(BuildContext context) { - return const Dialog( - child: Text('Create Space'), + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.25, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: SpaceIconPicker( + iconPath: space.icon, + ), + ), + Expanded( + flex: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text(context.watch().state.toString()), + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) { + final subspaces = space.subspaces; + if (subspaces.isEmpty) return false; + return subspaces.any( + (subspace) => subspace.name == value, + ); + }, + ), + const Spacer(), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], + ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart new file mode 100644 index 00000000..d5bd1016 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/extension/build_context_x.dart'; + +class SpaceNameTextField extends StatefulWidget { + const SpaceNameTextField({ + required this.initialValue, + required this.isNameFieldExist, + super.key, + }); + + final String? initialValue; + final bool Function(String value) isNameFieldExist; + + @override + State createState() => _SpaceNameTextFieldState(); +} + +class _SpaceNameTextFieldState extends State { + late final TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(text: widget.initialValue); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + + String? _validateName(String? value) { + if (value == null || value.isEmpty) { + return '*Space name should not be empty.'; + } + if (widget.isNameFieldExist(value)) { + return '*Name already exists'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + controller: _controller, + onChanged: (value) => context.read().add( + UpdateSpaceDetailsName(value), + ), + validator: _validateName, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.lightGrayColor, + ), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + ), + ), + errorStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart new file mode 100644 index 00000000..071d6fec --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.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/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceSubSpacesBox extends StatelessWidget { + const SpaceSubSpacesBox({super.key, required this.subspaces}); + + final List subspaces; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (subspaces.isEmpty) + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () => showDialog( + context: context, + builder: (_) => SpaceSubSpacesDialog(subspaces: subspaces), + ), + child: const ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Create Sub Spaces', + // disabled: widget.isTagsAndSubspaceModelDisabled, + disabled: false, + ), + ) + else + SizedBox( + width: context.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map( + (e) => SubspaceNameDisplayWidget(subSpace: e), + ), + EditChip( + onTap: () {}, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart new file mode 100644 index 00000000..1c435d8b --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/space_details/presentation/widgets/subspace_chip.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/extension/build_context_x.dart'; + +class SpaceSubSpacesDialog extends StatelessWidget { + const SpaceSubSpacesDialog({ + required this.subspaces, + super.key, + }); + + final List subspaces; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create Sub Spaces'), + content: TextFieldSubSpaceDialogWidget( + subSpaces: subspaces, + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () {}, + onCancel: Navigator.of(context).pop, + ) + ], + ); + } +} + +class TextFieldSubSpaceDialogWidget extends StatefulWidget { + const TextFieldSubSpaceDialogWidget({ + super.key, + required this.subSpaces, + }); + + final List subSpaces; + + @override + State createState() => _TextFieldSubSpaceDialogWidgetState(); +} + +class _TextFieldSubSpaceDialogWidgetState extends State { + + late final TextEditingController _subspaceNameController; + + @override + void initState() { + super.initState(); + _subspaceNameController = TextEditingController(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.name.toLowerCase(); + + final duplicateIndices = widget.subSpaces + .asMap() + .entries + .where((e) => e.value.name.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + onDeleted: () => context.read().add( + UpdateSpaceDetailsSubspaces( + widget.subSpaces.where((e) => e.uuid != subSpace.uuid).toList(), + ), + ), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + context.read().add( + UpdateSpaceDetailsSubspaces( + [ + ...widget.subSpaces, + Subspace( + name: trimmedValue, + uuid: '', + productAllocations: const [], + ), + ], + ), + ); + _subspaceNameController.clear(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart new file mode 100644 index 00000000..a80ddd15 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart @@ -0,0 +1,55 @@ +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/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceChip extends StatelessWidget { + const SubspaceChip({ + required this.subSpace, + required this.isDuplicate, + required this.onDeleted, + super.key, + }); + + final Subspace subSpace; + final bool isDuplicate; + final void Function() onDeleted; + + @override + Widget build(BuildContext context) { + return Chip( + label: Text( + subSpace.name, + style: context.textTheme.bodySmall?.copyWith( + color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), + ), + onDeleted: onDeleted, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart new file mode 100644 index 00000000..3d7ab59b --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/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/extension/build_context_x.dart'; + +class SubspaceNameDisplayWidget extends StatefulWidget { + const SubspaceNameDisplayWidget({super.key, required this.subSpace}); + + final Subspace subSpace; + + @override + State createState() => + _SubspaceNameDisplayWidgetState(); +} + +class _SubspaceNameDisplayWidgetState extends State { + late final TextEditingController _controller; + bool isEditing = false; + @override + void initState() { + _controller = TextEditingController(text: widget.subSpace.name); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.spaceColor, + ); + return InkWell( + onTap: () => setState(() => isEditing = true), + child: Visibility( + visible: isEditing, + replacement: Text( + widget.subSpace.name, + style: textStyle, + ), + child: TextField( + controller: _controller, + style: textStyle, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsetsDirectional.symmetric( + horizontal: 8, + ), + ), + onSubmitted: (value) { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid + ? e.copyWith(name: value) + : e, + ) + .toList(), + ), + ); + }, + ), + ), + ); + } +}