From 5f30a5a61b612a8a84ab4558d6d857f83a2363ee Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 23 Jun 2025 10:01:01 +0300 Subject: [PATCH 01/75] Refactor empty state widget to use a container for better layout control --- lib/pages/common/custom_table.dart | 55 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 625c59c2..fb8237b7 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -179,31 +179,36 @@ class _DynamicTableState extends State { ); } - Widget _buildEmptyState() => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - SvgPicture.asset(Assets.emptyTable), - const SizedBox(height: 15), - Text( - widget.tableName == 'AccessManagement' - ? 'No Password ' - : 'No Devices', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: ColorsManager.grayColor), - ) - ], - ), - ], - ), - ], + Widget _buildEmptyState() => Container( + height: widget.size.height, + color: ColorsManager.whiteColors, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox(height: 15), + Text( + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor), + ) + ], + ), + ], + ), + SizedBox(height: widget.size.height * 0.5), + ], + ), ); Widget _buildSelectAllCheckbox() { return Container( From ff3d5cd996d4493d2bf89dbc125c45b074d6528f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:02 +0300 Subject: [PATCH 02/75] Created a helper class to show create community dialog, since this dialog can be shown from two different widgets. --- ...ce_management_community_dialog_helper.dart | 24 +++++++++++++++++++ .../space_management_sidebar_header.dart | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart new file mode 100644 index 00000000..a18834df --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/create_community/presentation/create_community_dialog.dart'; + +abstract final class SpaceManagementCommunityDialogHelper { + static void showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index 25c094db..b5f2a1b7 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.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/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget { if (isSelected) { _clearSelection(context); } else { - _showCreateCommunityDialog(context); + SpaceManagementCommunityDialogHelper.showCreateDialog(context); } } @@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget { const ClearCommunitiesTreeSelectionEvent(), ); } - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } From 0e7109a19e297bb461a8214d01c504d5b0ab6098 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:15 +0300 Subject: [PATCH 03/75] Created `CommunityTemplateCell` widget. --- .../widgets/community_template_cell.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart new file mode 100644 index 00000000..4352d069 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityTemplateCell extends StatelessWidget { + const CommunityTemplateCell({ + super.key, + required this.onTap, + required this.title, + }); + + final void Function() onTap; + final Widget title; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 2.0, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 4, + strokeAlign: BorderSide.strokeAlignOutside, + color: ColorsManager.borderColor, + ), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), + ), + DefaultTextStyle( + style: context.textTheme.bodyLarge!.copyWith( + color: ColorsManager.blackColor, + ), + child: title, + ), + ], + ), + ); + } +} From a78b5993a9aa6c7e425474c0c23f30d3b3c2cadd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:05:53 +0300 Subject: [PATCH 04/75] Created `SpaceManagementTemplatesView` widget. --- .../space_management_templates_view.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart new file mode 100644 index 00000000..dd46d2c1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceManagementTemplatesView extends StatelessWidget { + const SpaceManagementTemplatesView({super.key}); + @override + Widget build(BuildContext context) { + return Expanded( + child: ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, + ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, + ), + ), + ); + } + + List<_CommunityTemplateModel> _gridItems(BuildContext context) { + return [ + _CommunityTemplateModel( + title: const Text('Blank'), + onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context), + ), + ]; + } +} + +class _CommunityTemplateModel { + final Widget title; + final void Function() onTap; + + _CommunityTemplateModel({ + required this.title, + required this.onTap, + }); +} From 7d4cdba0eff5c9e09ac075f98302ea9a8857c173 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:06:59 +0300 Subject: [PATCH 05/75] Connected templates view into `SpaceManagementBody`, while applying the correct UI principals if what to show what when? --- .../widgets/space_management_body.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 3a9aa3c8..5d28a533 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.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/communities/presentation/widgets/space_management_communities_tree.dart'; class SpaceManagementBody extends StatelessWidget { @@ -6,9 +9,21 @@ class SpaceManagementBody extends StatelessWidget { @override Widget build(BuildContext context) { - return const Row( + return Row( children: [ - SpaceManagementCommunitiesTree(), + const SpaceManagementCommunitiesTree(), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.selectedCommunity != current.selectedCommunity, + builder: (context, state) => Visibility( + visible: state.selectedCommunity == null, + replacement: const Placeholder(), + child: const SpaceManagementTemplatesView(), + ), + ), + ), ], ); } From f8e4c89cdb63e50ef6cc1323a298f603690f36cf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:03 +0300 Subject: [PATCH 06/75] uses correct error message that the api sends in `RemoteCreateCommunityService`. --- .../data/services/remote_create_community_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index bd91f6ce..aae92e9f 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService { return _defaultErrorMessage; } final error = body['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; return errorMessage; } From 4bdb487094fc81d9786acaacf2fcceccc8ee6f2b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:23 +0300 Subject: [PATCH 07/75] doesnt show a snackbar when creating a community fails, since we show the error message in the dialog itself. --- .../presentation/create_community_dialog.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index 8c1d474d..a9af44d6 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget { ); onCreateCommunity.call(community); break; - case CreateCommunityFailure(:final message): + case CreateCommunityFailure(): Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); break; default: break; From ada7daf17950efb2aba67d39efd867d796f2facd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:13:30 +0300 Subject: [PATCH 08/75] Switched from using `Text` to `SelectableText` in `CreateCommunityDialog`. --- .../helpers/space_management_community_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart index a18834df..5322c3ea 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -9,7 +9,7 @@ abstract final class SpaceManagementCommunityDialogHelper { showDialog( context: context, builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), + title: const SelectableText('Community Name'), onCreateCommunity: (community) { context.read().add( InsertCommunity(community), From 1200a809c2719f9da1f4c6d6e86b51d468981d45 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 14:33:56 +0300 Subject: [PATCH 09/75] now cant use offline device to controll --- .../widgets/device_managment_body.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index f4baad0c..c484b1b1 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -103,8 +103,30 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { decoration: containerDecoration, child: Center( child: DefaultButton( + backgroundColor: selectedDevices.any( + (element) => !element.online!, + ) + ? ColorsManager.primaryColor + .withValues(alpha: 0.1) + : null, onPressed: isControlButtonEnabled ? () { + if (selectedDevices.any( + (element) => !element.online!, + )) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'This Device is Offline', + ), + duration: + Duration(seconds: 2), + ), + ); + return; + } + if (selectedDevices.length == 1) { showDialog( context: context, From 71cf0a636e15c2009bd9f134ef158e685f07a9c0 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:22:24 +0300 Subject: [PATCH 10/75] send all user instead of only uuid --- .../users_table/view/users_page.dart | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 767fd9a6..88f89e30 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -19,6 +19,7 @@ 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'; import 'package:syncrow_web/utils/style.dart'; + class UsersPage extends StatelessWidget { UsersPage({super.key}); @@ -451,33 +452,33 @@ class UsersPage extends StatelessWidget { ), Row( children: [ - user.isEnabled != false - ? actionButton( - isActive: true, - title: "Edit", - onTap: () { - context - .read() - .add(ClearCachedData()); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EditUserDialog( - userId: user.uuid); - }, - ).then((v) { - if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } - } - }); + if (user.isEnabled != false) + actionButton( + isActive: true, + title: "Edit", + onTap: () { + context + .read() + .add(ClearCachedData()); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog(user: user); }, - ) - : actionButton( - title: "Edit", - ), + ).then((v) { + if (v != null) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + } + }); + }, + ) + else + actionButton( + title: "Edit", + ), actionButton( title: "Delete", onTap: () { From e6957d566da8ec3e4a24dc9e23ffe7d47eebb290 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:29:35 +0300 Subject: [PATCH 11/75] use RoleUserModel instead of only id and all is good --- .../view/edit_user_dialog.dart | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index 071de067..c84a6baf 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; @@ -12,8 +14,11 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class EditUserDialog extends StatefulWidget { - final String? userId; - const EditUserDialog({super.key, this.userId}); + final RolesUserModel? user; + const EditUserDialog({ + super.key, + this.user, + }); @override _EditUserDialogState createState() => _EditUserDialogState(); @@ -28,10 +33,11 @@ class _EditUserDialogState extends State { create: (BuildContext context) => UsersBloc() // ..add(const LoadCommunityAndSpacesEvent()) ..add(const RoleEvent()) - ..add(GetUserByIdEvent(uuid: widget.userId)), + ..add(GetUserByIdEvent(uuid: widget.user!.uuid)), child: BlocConsumer(listener: (context, state) { if (state is SpacesLoadedState) { - BlocProvider.of(context).add(GetUserByIdEvent(uuid: widget.userId)); + BlocProvider.of(context) + .add(GetUserByIdEvent(uuid: widget.user!.uuid)); } }, builder: (context, state) { final _blocRole = BlocProvider.of(context); @@ -39,7 +45,8 @@ class _EditUserDialogState extends State { return Dialog( child: Container( decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), width: 900, child: Column( children: [ @@ -68,7 +75,8 @@ class _EditUserDialogState extends State { children: [ _buildStep1Indicator(1, "Basics", _blocRole), _buildStep2Indicator(2, "Spaces", _blocRole), - _buildStep3Indicator(3, "Role & Permissions", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), ], ), ), @@ -86,7 +94,7 @@ class _EditUserDialogState extends State { children: [ const SizedBox(height: 10), Expanded( - child: _getFormContent(widget.userId), + child: _getFormContent(widget.user!), ), const SizedBox(height: 20), ], @@ -116,13 +124,14 @@ class _EditUserDialogState extends State { if (currentStep < 3) { currentStep++; if (currentStep == 2) { - _blocRole.add(CheckStepStatus(isEditUser: true)); + _blocRole + .add(CheckStepStatus(isEditUser: true)); } else if (currentStep == 3) { _blocRole.add(const CheckSpacesStepStatus()); } } else { - _blocRole - .add(EditInviteUsers(context: context, userId: widget.userId!)); + _blocRole.add(EditInviteUsers( + context: context, userId: widget.user!.uuid)); } }); }, @@ -131,7 +140,8 @@ class _EditUserDialogState extends State { style: TextStyle( color: (_blocRole.isCompleteSpaces == false || _blocRole.isCompleteBasics == false || - _blocRole.isCompleteRolePermissions == false) && + _blocRole.isCompleteRolePermissions == + false) && currentStep == 3 ? ColorsManager.grayColor : ColorsManager.secondaryColor), @@ -146,15 +156,15 @@ class _EditUserDialogState extends State { })); } - Widget _getFormContent(userid) { + Widget _getFormContent(RolesUserModel user) { switch (currentStep) { case 1: return BasicsView( - userId: userid, + userId: user.uuid, ); case 2: return SpacesAccessView( - userId: userid, + userId: user.uuid, ); case 3: return const RolesAndPermission(); @@ -204,8 +214,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -263,8 +277,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -321,8 +339,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], From 8bc7a3daa2b6cc3fdd44767186f6536ad6b8a0af Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 15:45:49 +0300 Subject: [PATCH 12/75] Implemented space management canvas. --- .../models/space_connection_model.dart | 6 + .../spaces_connections_arrow_painter.dart | 60 +++++ .../widgets/community_structure_canvas.dart | 236 ++++++++++++++++++ .../widgets/create_space_button.dart | 42 ++++ .../widgets/plus_button_widget.dart | 43 ++++ .../widgets/space_card_widget.dart | 65 +++++ .../main_module/widgets/space_cell.dart | 88 +++++++ .../widgets/space_management_body.dart | 3 +- .../space_management_community_structure.dart | 22 ++ .../communities_tree_selection_bloc.dart | 2 +- .../communities_tree_selection_event.dart | 7 +- ...anagement_communities_tree_space_tile.dart | 2 +- lib/utils/app_routes.dart | 2 +- 13 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/models/space_connection_model.dart create mode 100644 lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/create_space_button.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_cell.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart diff --git a/lib/pages/space_management_v2/main_module/models/space_connection_model.dart b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart new file mode 100644 index 00000000..538a922c --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart @@ -0,0 +1,6 @@ +class SpaceConnectionModel { + final String from; + final String to; + + const SpaceConnectionModel({required this.from, required this.to}); +} diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart new file mode 100644 index 00000000..fcf523bf --- /dev/null +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesConnectionsArrowPainter extends CustomPainter { + final List connections; + final Map positions; + final double cardWidth = 150.0; + final double cardHeight = 90.0; + final String? selectedSpaceUuid; + + SpacesConnectionsArrowPainter({ + required this.connections, + required this.positions, + this.selectedSpaceUuid, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final connection in connections) { + final isSelected = connection.to == selectedSpaceUuid; + final paint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + final from = positions[connection.from]; + final to = positions[connection.to]; + + if (from != null && to != null) { + final startPoint = + Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); + final endPoint = Offset(to.dx + cardWidth / 2, to.dy); + + final path = Path()..moveTo(startPoint.dx, startPoint.dy); + + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); + + path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, + controlPoint2.dy, endPoint.dx, endPoint.dy); + + canvas.drawPath(path, paint); + + final circlePaint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.srcIn; + canvas.drawCircle(endPoint, 4, circlePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart new file mode 100644 index 00000000..92c5add6 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class CommunityStructureCanvas extends StatefulWidget { + const CommunityStructureCanvas({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + State createState() =>_CommunityStructureCanvasState(); +} + +class _CommunityStructureCanvasState extends State + with SingleTickerProviderStateMixin { + final Map _positions = {}; + final double _cardWidth = 150.0; + final double _cardHeight = 90.0; + final double _horizontalSpacing = 150.0; + final double _verticalSpacing = 120.0; + String? _selectedSpaceUuid; + + late TransformationController _transformationController; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _transformationController = TransformationController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + } + + @override + void dispose() { + _transformationController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _runAnimation(Matrix4 target) { + final animation = Matrix4Tween( + begin: _transformationController.value, + end: target, + ).animate(_animationController); + + void listener() { + _transformationController.value = animation.value; + } + + animation.addListener(listener); + _animationController.forward(from: 0).whenCompleteOrCancel(() { + animation.removeListener(listener); + }); + } + + void _onSpaceTapped(String spaceUuid) { + setState(() { + _selectedSpaceUuid = spaceUuid; + }); + + final position = _positions[spaceUuid]; + if (position == null) return; + + const scale = 2.0; + final viewSize = context.size; + if (viewSize == null) return; + + final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); + final y = + -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); + + final matrix = Matrix4.identity() + ..translate(x, y) + ..scale(scale); + + _runAnimation(matrix); + } + + void _resetSelectionAndZoom() { + setState(() { + _selectedSpaceUuid = null; + }); + _runAnimation(Matrix4.identity()); + } + + void _calculateLayout( + List spaces, + int depth, + Map levelXOffset, + ) { + for (final space in spaces) { + double childSubtreeWidth = 0; + if (space.children.isNotEmpty) { + _calculateLayout(space.children, depth + 1, levelXOffset); + final firstChildPos = _positions[space.children.first.uuid]; + final lastChildPos = _positions[space.children.last.uuid]; + if (firstChildPos != null && lastChildPos != null) { + childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; + } + } + + final currentX = levelXOffset.putIfAbsent(depth, () => 0.0); + double? x; + + if (space.children.isNotEmpty) { + final firstChildPos = _positions[space.children.first.uuid]!; + x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; + } else { + x = currentX; + } + + if (x < currentX) { + final shiftX = currentX - x; + _shiftSubtree(space, shiftX); + final keysToShift = levelXOffset.keys.where((d) => d > depth).toList(); + for (final key in keysToShift) { + levelXOffset[key] = levelXOffset[key]! + shiftX; + } + x += shiftX; + } + + final y = depth * (_verticalSpacing + _cardHeight); + _positions[space.uuid] = Offset(x, y); + levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; + } + } + + void _shiftSubtree(SpaceModel space, double shiftX) { + if (_positions.containsKey(space.uuid)) { + _positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0); + } + for (final child in space.children) { + _shiftSubtree(child, shiftX); + } + } + + List _buildTreeWidgets() { + _positions.clear(); + final community = widget.community; + + _calculateLayout(community.spaces, 0, {}); + + final widgets = []; + final connections = []; + _generateWidgets(community.spaces, widgets, connections); + + return [ + CustomPaint( + painter: SpacesConnectionsArrowPainter( + connections: connections, + positions: _positions, + selectedSpaceUuid: _selectedSpaceUuid, + ), + child: Stack(alignment: AlignmentDirectional.center, children: widgets), + ), + ]; + } + + void _generateWidgets( + List spaces, + List widgets, + List connections, + ) { + for (final space in spaces) { + final position = _positions[space.uuid]; + if (position == null) continue; + + widgets.add( + Positioned( + left: position.dx, + top: position.dy, + width: _cardWidth, + height: _cardHeight, + child: SpaceCardWidget( + index: spaces.indexOf(space), + onPositionChanged: (newPosition) {}, + buildSpaceContainer: (index) { + return Opacity( + opacity: 1.0, + child: SpaceCell( + index: index, + onTap: () => _onSpaceTapped(space.uuid), + icon: space.icon, + name: space.spaceName, + ), + ); + }, + screenSize: MediaQuery.sizeOf(context), + position: position, + isHovered: false, + onHoverChanged: (int index, bool isHovered) {}, + onButtonTap: (int index, Offset newPosition) {}, + ), + ), + ); + + for (final child in space.children) { + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + _generateWidgets(space.children, widgets, connections); + } + } + + @override + Widget build(BuildContext context) { + final treeWidgets = _buildTreeWidgets(); + return InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: MediaQuery.sizeOf(context).width * 0.3, + vertical: MediaQuery.sizeOf(context).height * 0.2, + ), + minScale: 0.5, + maxScale: 3.0, + constrained: false, + child: GestureDetector( + onTap: _resetSelectionAndZoom, + child: SizedBox( + width: MediaQuery.sizeOf(context).width * 2, + height: MediaQuery.sizeOf(context).height * 2, + child: Stack(children: treeWidgets), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart new file mode 100644 index 00000000..5caf6a81 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSpaceButton extends StatelessWidget { + const CreateSpaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () {}, + child: Container( + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart new file mode 100644 index 00000000..755a6ab9 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PlusButtonWidget extends StatelessWidget { + final int index; + final String direction; + final Offset offset; + final void Function(int index, Offset newPosition) onButtonTap; + + const PlusButtonWidget({ + super.key, + required this.index, + required this.direction, + required this.offset, + required this.onButtonTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (direction == 'down') { + onButtonTap(index, const Offset(0, 150)); + } else { + onButtonTap(index, const Offset(150, 0)); + } + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart new file mode 100644 index 00000000..1ce28502 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; + +class SpaceCardWidget extends StatelessWidget { + final int index; + final Size screenSize; + final Offset position; + final bool isHovered; + final void Function(int index, bool isHovered) onHoverChanged; + final void Function(int index, Offset newPosition) onButtonTap; + final Widget Function(int index) buildSpaceContainer; + final ValueChanged onPositionChanged; + + const SpaceCardWidget({ + super.key, + required this.index, + required this.onPositionChanged, + required this.screenSize, + required this.position, + required this.isHovered, + required this.onHoverChanged, + required this.onButtonTap, + required this.buildSpaceContainer, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => onHoverChanged(index, true), + onExit: (_) => onHoverChanged(index, false), + child: SizedBox( + width: 150, + height: 90, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + buildSpaceContainer(index), + + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + index: index, + direction: 'down', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + if (isHovered) + Positioned( + right: -15, + child: PlusButtonWidget( + index: index, + direction: 'right', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart new file mode 100644 index 00000000..1b08835a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceCell extends StatelessWidget { + final int index; + final String icon; + final String name; + final VoidCallback? onDoubleTap; + final VoidCallback? onTap; + + const SpaceCell({ + super.key, + required this.index, + required this.icon, + required this.name, + this.onTap, + this.onDoubleTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GestureDetector( + onDoubleTap: onDoubleTap, + onTap: onTap, + child: Container( + width: 150, + height: 70, + decoration: _containerDecoration(), + child: Row( + children: [ + _buildIconContainer(), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } + + Widget _buildIconContainer() { + return Container( + width: 40, + height: double.infinity, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + color: ColorsManager.whiteColors, + width: 24, + height: 24, + ), + ), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: ColorsManager.lightGrayColor.withValues(alpha: 0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 5d28a533..5d81bffb 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.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/communities/presentation/widgets/space_management_communities_tree.dart'; @@ -19,7 +20,7 @@ class SpaceManagementBody extends StatelessWidget { previous.selectedCommunity != current.selectedCommunity, builder: (context, state) => Visibility( visible: state.selectedCommunity == null, - replacement: const Placeholder(), + replacement: const SpaceManagementCommunityStructure(), child: const SpaceManagementTemplatesView(), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart new file mode 100644 index 00000000..11ee5078 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; + +class SpaceManagementCommunityStructure extends StatelessWidget { + const SpaceManagementCommunityStructure({super.key}); + + @override + Widget build(BuildContext context) { + final selectedCommunity = + context.watch().state.selectedCommunity!; + const spacer = Spacer(flex: 10); + return Visibility( + visible: selectedCommunity.spaces.isNotEmpty, + replacement: const Row( + children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + child: CommunityStructureCanvas(community: selectedCommunity), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart index bfc02f11..bdda04ee 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc ) { emit( CommunitiesTreeSelectionState( - selectedCommunity: null, + selectedCommunity: event.community, selectedSpace: event.space, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 95ffe173..40a41f74 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable { } final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { - final CommunityModel? community; + final CommunityModel community; const SelectCommunityEvent({required this.community}); @override @@ -16,9 +16,10 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel? space; + final SpaceModel space; + final CommunityModel community; - const SelectSpaceEvent({required this.space}); + const SelectSpaceEvent({required this.space, required this.community}); @override List get props => [space]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart index dcd44ac8..795e2c3a 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { initiallyExpanded: spaceIsExpanded, onExpansionChanged: (expanded) {}, onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), + SelectSpaceEvent(community: community, space: space), ), children: space.children .map( diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 5276f4186c46022f5bf42cc6e9aa23fba8cf8ddb Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:55:56 +0300 Subject: [PATCH 13/75] emit error state when catch error and send the real API exception --- lib/pages/auth/bloc/auth_bloc.dart | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 58950089..bfe0b3eb 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -36,7 +36,8 @@ class AuthBloc extends Bloc { ////////////////////////////// forget password ////////////////////////////////// final TextEditingController forgetEmailController = TextEditingController(); - final TextEditingController forgetPasswordController = TextEditingController(); + final TextEditingController forgetPasswordController = + TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); final forgetEmailKey = GlobalKey(); @@ -53,7 +54,8 @@ class AuthBloc extends Bloc { return; } _remainingTime = 1; - add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false)); + add(UpdateTimerEvent( + remainingTime: _remainingTime, isButtonEnabled: false)); try { forgetEmailValidate = ''; _remainingTime = (await AuthenticationAPI.sendOtp( @@ -90,7 +92,8 @@ class AuthBloc extends Bloc { _timer?.cancel(); add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true)); } else { - add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false)); + add(UpdateTimerEvent( + remainingTime: _remainingTime, isButtonEnabled: false)); } }); } @@ -100,7 +103,7 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } -Future changePassword( + Future changePassword( ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { @@ -122,7 +125,6 @@ Future changePassword( } } - String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -131,7 +133,9 @@ Future changePassword( } void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) { - emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime)); + emit(TimerState( + isButtonEnabled: event.isButtonEnabled, + remainingTime: event.remainingTime)); } ///////////////////////////////////// login ///////////////////////////////////// @@ -151,7 +155,6 @@ Future changePassword( static UserModel? user; bool showValidationMessage = false; - void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -170,11 +173,11 @@ Future changePassword( ); } on APIException catch (e) { validate = e.message; - emit(LoginInitial()); + emit(LoginFailure(error: validate)); return; } catch (e) { validate = 'Something went wrong'; - emit(LoginInitial()); + emit(LoginFailure(error: validate)); return; } @@ -197,7 +200,6 @@ Future changePassword( } } - checkBoxToggle( CheckBoxEvent event, Emitter emit, @@ -339,12 +341,14 @@ Future changePassword( static Future getTokenAndValidate() async { try { const storage = FlutterSecureStorage(); - final firstLaunch = - await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; + final firstLaunch = await SharedPreferencesHelper.readBoolFromSP( + StringsManager.firstLaunch) ?? + true; if (firstLaunch) { storage.deleteAll(); } - await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false); + await SharedPreferencesHelper.saveBoolToSP( + StringsManager.firstLaunch, false); final value = await storage.read(key: Token.loginAccessTokenKey) ?? ''; if (value.isEmpty) { return 'Token not found'; @@ -397,7 +401,9 @@ Future changePassword( final String formattedTime = [ if (days > 0) '${days}d', // Append 'd' for days if (days > 0 || hours > 0) - hours.toString().padLeft(2, '0'), // Show hours if there are days or hours + hours + .toString() + .padLeft(2, '0'), // Show hours if there are days or hours minutes.toString().padLeft(2, '0'), seconds.toString().padLeft(2, '0'), ].join(':'); From ad00cf35ba65e6a935b4203289dfe6bbe0f8ac29 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:05:16 +0300 Subject: [PATCH 14/75] added the PR notes --- .../all_devices/widgets/device_managment_body.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index c484b1b1..c865a5dc 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -62,7 +62,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; - + final isAnyDeviceOffline = + selectedDevices.any((element) => !(element.online ?? false)); return Row( children: [ Expanded(child: SpaceTreeView( @@ -103,17 +104,13 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { decoration: containerDecoration, child: Center( child: DefaultButton( - backgroundColor: selectedDevices.any( - (element) => !element.online!, - ) + backgroundColor: isAnyDeviceOffline ? ColorsManager.primaryColor .withValues(alpha: 0.1) : null, onPressed: isControlButtonEnabled ? () { - if (selectedDevices.any( - (element) => !element.online!, - )) { + if (isAnyDeviceOffline) { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( From 379ecec789e2339fa3e4e4b07419155384991c41 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:15:05 +0300 Subject: [PATCH 15/75] use isCurrentStep instead of checking with multi variables everyTime --- .../view/edit_user_dialog.dart | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index c84a6baf..00c566c6 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -176,6 +176,7 @@ class _EditUserDialogState extends State { int step3 = 0; Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -199,7 +200,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteBasics == false ? Assets.wrongProcessIcon @@ -214,12 +215,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -243,6 +243,7 @@ class _EditUserDialogState extends State { } Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -262,7 +263,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteSpaces == false ? Assets.wrongProcessIcon @@ -277,12 +278,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -306,6 +306,7 @@ class _EditUserDialogState extends State { } Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -324,7 +325,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteRolePermissions == false ? Assets.wrongProcessIcon @@ -339,12 +340,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], From d14cc785a8f23dda61d274cf8b22d4f08bbb4c90 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:16:34 +0300 Subject: [PATCH 16/75] no need for two condtions inside themselves --- .../users_page/users_table/view/users_page.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 88f89e30..da159d94 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -468,9 +468,7 @@ class UsersPage extends StatelessWidget { }, ).then((v) { if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } + _blocRole.add(const GetUsers()); } }); }, From 75efc595b47b6ef4eec385e57b1dd8f26158ad08 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 16:22:11 +0300 Subject: [PATCH 17/75] reverted to old import to avoid confusion with QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 95d6e1ecda4709a693e39f6101ca95af69207f60 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:33:45 +0300 Subject: [PATCH 18/75] if online go green with online status else red with offline status --- .../shared/device_control_dialog.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index beb3b52c..7a046bea 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -79,6 +79,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { } Widget _buildDeviceInfoSection() { + final isOnlineDevice = device.online != null && device.online!; return Padding( padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50), child: Table( @@ -107,7 +108,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { 'Installation Date and Time:', formatDateTime( DateTime.fromMillisecondsSinceEpoch( - ((device.createTime ?? 0) * 1000), + (device.createTime ?? 0) * 1000, ), ), ), @@ -126,12 +127,16 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), TableRow( children: [ - _buildInfoRow('Status:', 'Online', statusColor: Colors.green), + _buildInfoRow( + 'Status:', + isOnlineDevice ? 'Online' : 'offline', + statusColor: isOnlineDevice ? Colors.green : Colors.red, + ), _buildInfoRow( 'Last Offline Date and Time:', formatDateTime( DateTime.fromMillisecondsSinceEpoch( - ((device.updateTime ?? 0) * 1000), + (device.updateTime ?? 0) * 1000, ), ), ), From 95ae50d12dd97ec616d22f0b14d9ab70c2ea7920 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:16:03 +0300 Subject: [PATCH 19/75] navigates to selected space when changed on sidebar in space management canvas. --- .../widgets/community_structure_canvas.dart | 82 ++++++++++++------- .../widgets/plus_button_widget.dart | 14 +--- .../widgets/space_card_widget.dart | 51 ++++-------- .../main_module/widgets/space_cell.dart | 17 ++-- .../space_management_community_structure.dart | 12 ++- .../communities_tree_selection_event.dart | 2 +- 6 files changed, 86 insertions(+), 92 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 92c5add6..22b4536a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.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/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ required this.community, + required this.selectedSpace, super.key, }); final CommunityModel community; + final SpaceModel? selectedSpace; @override - State createState() =>_CommunityStructureCanvasState(); + State createState() => _CommunityStructureCanvasState(); } class _CommunityStructureCanvasState extends State @@ -25,19 +29,30 @@ class _CommunityStructureCanvasState extends State final double _cardHeight = 90.0; final double _horizontalSpacing = 150.0; final double _verticalSpacing = 120.0; - String? _selectedSpaceUuid; late TransformationController _transformationController; late AnimationController _animationController; @override void initState() { - super.initState(); _transformationController = TransformationController(); _animationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 100), + duration: const Duration(milliseconds: 150), ); + super.initState(); + } + + @override + void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _animateToSpace(widget.selectedSpace); + } + }); + } } @override @@ -63,15 +78,16 @@ class _CommunityStructureCanvasState extends State }); } - void _onSpaceTapped(String spaceUuid) { - setState(() { - _selectedSpaceUuid = spaceUuid; - }); + void _animateToSpace(SpaceModel? space) { + if (space == null) { + _runAnimation(Matrix4.identity()); + return; + } - final position = _positions[spaceUuid]; + final position = _positions[space.uuid]; if (position == null) return; - const scale = 2.0; + const scale = 1.5; final viewSize = context.size; if (viewSize == null) return; @@ -86,11 +102,19 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _onSpaceTapped(SpaceModel? space) { + context.read().add( + SelectSpaceEvent(community: widget.community, space: space), + ); + } + void _resetSelectionAndZoom() { - setState(() { - _selectedSpaceUuid = null; - }); - _runAnimation(Matrix4.identity()); + context.read().add( + SelectSpaceEvent( + community: widget.community, + space: null, + ), + ); } void _calculateLayout( @@ -159,7 +183,7 @@ class _CommunityStructureCanvasState extends State painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, - selectedSpaceUuid: _selectedSpaceUuid, + selectedSpaceUuid: widget.selectedSpace?.uuid, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), ), @@ -182,24 +206,24 @@ class _CommunityStructureCanvasState extends State width: _cardWidth, height: _cardHeight, child: SpaceCardWidget( - index: spaces.indexOf(space), - onPositionChanged: (newPosition) {}, - buildSpaceContainer: (index) { + buildSpaceContainer: () { return Opacity( - opacity: 1.0, + opacity: widget.selectedSpace == null + ? 1.0 + : widget.selectedSpace?.uuid != space.uuid + ? 0.5 + : 1.0, child: SpaceCell( - index: index, - onTap: () => _onSpaceTapped(space.uuid), + onTap: () => _onSpaceTapped(space), icon: space.icon, name: space.spaceName, ), ); }, - screenSize: MediaQuery.sizeOf(context), - position: position, - isHovered: false, - onHoverChanged: (int index, bool isHovered) {}, - onButtonTap: (int index, Offset newPosition) {}, + onTap: () => showDialog( + context: context, + builder: (context) => const Text('123'), + ), ), ), ); @@ -218,7 +242,7 @@ class _CommunityStructureCanvasState extends State transformationController: _transformationController, boundaryMargin: EdgeInsets.symmetric( horizontal: MediaQuery.sizeOf(context).width * 0.3, - vertical: MediaQuery.sizeOf(context).height * 0.2, + vertical: MediaQuery.sizeOf(context).height * 0.3, ), minScale: 0.5, maxScale: 3.0, @@ -226,8 +250,8 @@ class _CommunityStructureCanvasState extends State child: GestureDetector( onTap: _resetSelectionAndZoom, child: SizedBox( - width: MediaQuery.sizeOf(context).width * 2, - height: MediaQuery.sizeOf(context).height * 2, + width: MediaQuery.sizeOf(context).width * 5, + height: MediaQuery.sizeOf(context).height * 5, child: Stack(children: treeWidgets), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart index 755a6ab9..68169861 100644 --- a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -2,15 +2,11 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class PlusButtonWidget extends StatelessWidget { - final int index; - final String direction; final Offset offset; - final void Function(int index, Offset newPosition) onButtonTap; + final void Function() onButtonTap; const PlusButtonWidget({ super.key, - required this.index, - required this.direction, required this.offset, required this.onButtonTap, }); @@ -18,13 +14,7 @@ class PlusButtonWidget extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - if (direction == 'down') { - onButtonTap(index, const Offset(0, 150)); - } else { - onButtonTap(index, const Offset(150, 0)); - } - }, + onTap: onButtonTap, child: Container( width: 30, height: 30, diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart index 1ce28502..e91e577f 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -1,60 +1,39 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; -class SpaceCardWidget extends StatelessWidget { - final int index; - final Size screenSize; - final Offset position; - final bool isHovered; - final void Function(int index, bool isHovered) onHoverChanged; - final void Function(int index, Offset newPosition) onButtonTap; - final Widget Function(int index) buildSpaceContainer; - final ValueChanged onPositionChanged; +class SpaceCardWidget extends StatefulWidget { + final void Function() onTap; + final Widget Function() buildSpaceContainer; const SpaceCardWidget({ - super.key, - required this.index, - required this.onPositionChanged, - required this.screenSize, - required this.position, - required this.isHovered, - required this.onHoverChanged, - required this.onButtonTap, + required this.onTap, required this.buildSpaceContainer, + super.key, }); + @override + State createState() => _SpaceCardWidgetState(); +} + +class _SpaceCardWidgetState extends State { + bool isHovered = false; @override Widget build(BuildContext context) { return MouseRegion( - onEnter: (_) => onHoverChanged(index, true), - onExit: (_) => onHoverChanged(index, false), + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), child: SizedBox( - width: 150, - height: 90, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ - buildSpaceContainer(index), - + widget.buildSpaceContainer(), if (isHovered) Positioned( bottom: 0, child: PlusButtonWidget( - index: index, - direction: 'down', offset: Offset.zero, - onButtonTap: onButtonTap, - ), - ), - if (isHovered) - Positioned( - right: -15, - child: PlusButtonWidget( - index: index, - direction: 'right', - offset: Offset.zero, - onButtonTap: onButtonTap, + onButtonTap: widget.onTap, ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart index 1b08835a..bcde6560 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -1,29 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceCell extends StatelessWidget { - final int index; final String icon; final String name; - final VoidCallback? onDoubleTap; final VoidCallback? onTap; const SpaceCell({ super.key, - required this.index, required this.icon, required this.name, - this.onTap, - this.onDoubleTap, + required this.onTap, }); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return GestureDetector( - onDoubleTap: onDoubleTap, onTap: onTap, child: Container( width: 150, @@ -36,7 +30,7 @@ class SpaceCell extends StatelessWidget { Expanded( child: Text( name, - style: theme.textTheme.bodyLarge?.copyWith( + style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, color: ColorsManager.blackColor, ), @@ -63,7 +57,10 @@ class SpaceCell extends StatelessWidget { child: Center( child: SvgPicture.asset( icon, - color: ColorsManager.whiteColors, + colorFilter: const ColorFilter.mode( + ColorsManager.whiteColors, + BlendMode.srcIn, + ), width: 24, height: 24, ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 11ee5078..6fe80835 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -9,14 +9,18 @@ class SpaceManagementCommunityStructure extends StatelessWidget { @override Widget build(BuildContext context) { - final selectedCommunity = - context.watch().state.selectedCommunity!; + final selectionBloc = context.watch().state; + final selectedCommunity = selectionBloc.selectedCommunity; + final selectedSpace = selectionBloc.selectedSpace; const spacer = Spacer(flex: 10); return Visibility( - visible: selectedCommunity.spaces.isNotEmpty, + visible: selectedCommunity!.spaces.isNotEmpty, replacement: const Row( children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), - child: CommunityStructureCanvas(community: selectedCommunity), + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), ); } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 40a41f74..21088632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -16,7 +16,7 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel space; + final SpaceModel? space; final CommunityModel community; const SelectSpaceEvent({required this.space, required this.community}); From 87b45fff1dfee05f8ae716ca7eed31589864d6db Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:21:25 +0300 Subject: [PATCH 20/75] removed expanded widget that caused a size exception. --- .../space_management_templates_view.dart | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart index dd46d2c1..138dbbc4 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -7,26 +7,24 @@ class SpaceManagementTemplatesView extends StatelessWidget { const SpaceManagementTemplatesView({super.key}); @override Widget build(BuildContext context) { - return Expanded( - child: ColoredBox( - color: ColorsManager.whiteColors, - child: GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 400, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 2.0, - ), - itemCount: _gridItems(context).length, - itemBuilder: (context, index) { - final model = _gridItems(context)[index]; - return CommunityTemplateCell( - onTap: model.onTap, - title: model.title, - ); - }, + return ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, ), ); } From 0fb91496135e059fc00d2aea2f13f62b253a4ec1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:26:56 +0300 Subject: [PATCH 21/75] Selects all children of a space when selecting a parent. --- .../spaces_connections_arrow_painter.dart | 10 +++--- .../widgets/community_structure_canvas.dart | 36 ++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index fcf523bf..cad82a60 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -7,21 +7,21 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final Map positions; final double cardWidth = 150.0; final double cardHeight = 90.0; - final String? selectedSpaceUuid; + final Set highlightedUuids; SpacesConnectionsArrowPainter({ required this.connections, required this.positions, - this.selectedSpaceUuid, + required this.highlightedUuids, }); @override void paint(Canvas canvas, Size size) { for (final connection in connections) { - final isSelected = connection.to == selectedSpaceUuid; + final isSelected = highlightedUuids.contains(connection.from); final paint = Paint() ..color = isSelected - ? ColorsManager.primaryColor + ? ColorsManager.blackColor : ColorsManager.blackColor.withValues(alpha: 0.5) ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; @@ -46,7 +46,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final circlePaint = Paint() ..color = isSelected - ? ColorsManager.primaryColor + ? ColorsManager.blackColor : ColorsManager.blackColor.withValues(alpha: 0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.srcIn; diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 22b4536a..04795c11 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -62,6 +62,15 @@ class _CommunityStructureCanvasState extends State super.dispose(); } + Set _getAllDescendantUuids(SpaceModel space) { + final uuids = {}; + for (final child in space.children) { + uuids.add(child.uuid); + uuids.addAll(_getAllDescendantUuids(child)); + } + return uuids; + } + void _runAnimation(Matrix4 target) { final animation = Matrix4Tween( begin: _transformationController.value, @@ -174,16 +183,23 @@ class _CommunityStructureCanvasState extends State _calculateLayout(community.spaces, 0, {}); + final selectedSpace = widget.selectedSpace; + final highlightedUuids = {}; + if (selectedSpace != null) { + highlightedUuids.add(selectedSpace.uuid); + highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace)); + } + final widgets = []; final connections = []; - _generateWidgets(community.spaces, widgets, connections); + _generateWidgets(community.spaces, widgets, connections, highlightedUuids); return [ CustomPaint( painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, - selectedSpaceUuid: widget.selectedSpace?.uuid, + highlightedUuids: highlightedUuids, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), ), @@ -194,11 +210,15 @@ class _CommunityStructureCanvasState extends State List spaces, List widgets, List connections, + Set highlightedUuids, ) { for (final space in spaces) { final position = _positions[space.uuid]; if (position == null) continue; + final isHighlighted = highlightedUuids.contains(space.uuid); + final hasNoSelectedSpace = widget.selectedSpace == null; + widgets.add( Positioned( left: position.dx, @@ -208,11 +228,7 @@ class _CommunityStructureCanvasState extends State child: SpaceCardWidget( buildSpaceContainer: () { return Opacity( - opacity: widget.selectedSpace == null - ? 1.0 - : widget.selectedSpace?.uuid != space.uuid - ? 0.5 - : 1.0, + opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, child: SpaceCell( onTap: () => _onSpaceTapped(space), icon: space.icon, @@ -229,9 +245,11 @@ class _CommunityStructureCanvasState extends State ); for (final child in space.children) { - connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + connections.add( + SpaceConnectionModel(from: space.uuid, to: child.uuid), + ); } - _generateWidgets(space.children, widgets, connections); + _generateWidgets(space.children, widgets, connections, highlightedUuids); } } From 329b2ba472ca1f0b4591ef4142911eb35c63f6df Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:36:13 +0300 Subject: [PATCH 22/75] selects the space from and to connection when selecting a space. --- .../painters/spaces_connections_arrow_painter.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index cad82a60..e9fa0a15 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -18,7 +18,8 @@ class SpacesConnectionsArrowPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { for (final connection in connections) { - final isSelected = highlightedUuids.contains(connection.from); + final isSelected = highlightedUuids.contains(connection.from) || + highlightedUuids.contains(connection.to); final paint = Paint() ..color = isSelected ? ColorsManager.blackColor @@ -36,7 +37,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final path = Path()..moveTo(startPoint.dx, startPoint.dy); - final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, From 90f8305aa1baaee3536000415ff5bf79d739c9a0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:45:13 +0300 Subject: [PATCH 23/75] shows tooltip on `SpaceCell`. --- .../widgets/community_structure_canvas.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 04795c11..5d38cfa2 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -229,10 +229,14 @@ class _CommunityStructureCanvasState extends State buildSpaceContainer: () { return Opacity( opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, - child: SpaceCell( - onTap: () => _onSpaceTapped(space), - icon: space.icon, - name: space.spaceName, + child: Tooltip( + message: space.spaceName, + preferBelow: false, + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, + ), ), ); }, From 5a2299ea2f166cd812409af66f0803be252f4d5f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:47:48 +0300 Subject: [PATCH 24/75] navigates to initial create space dialog from the respective buttons. --- .../widgets/community_structure_canvas.dart | 6 ++---- .../main_module/widgets/create_space_button.dart | 3 ++- .../space_management_community_structure.dart | 3 ++- .../helpers/space_details_dialog_helper.dart | 11 +++++++++++ .../presentation/widgets/space_details_dialog.dart | 12 ++++++++++++ 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 5d38cfa2..4aea103a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.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/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ @@ -240,10 +241,7 @@ class _CommunityStructureCanvasState extends State ), ); }, - onTap: () => showDialog( - context: context, - builder: (context) => const Text('123'), - ), + onTap: () => SpaceDetailsDialogHelper.showCreate(context), ), ), ); diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index 5caf6a81..4cbfd7fd 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CreateSpaceButton extends StatelessWidget { @@ -7,7 +8,7 @@ class CreateSpaceButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () {}, + onTap: () => SpaceDetailsDialogHelper.showCreate(context), child: Container( height: 60, decoration: BoxDecoration( diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 6fe80835..99d0668a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -16,7 +16,8 @@ class SpaceManagementCommunityStructure extends StatelessWidget { return Visibility( visible: selectedCommunity!.spaces.isNotEmpty, replacement: const Row( - children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + children: [spacer, Expanded(child: CreateSpaceButton()), spacer], + ), child: CommunityStructureCanvas( community: selectedCommunity, selectedSpace: selectedSpace, 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 new file mode 100644 index 00000000..e871f4d0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.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(), + ); + } +} 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 new file mode 100644 index 00000000..7213c99e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class SpaceDetailsDialog extends StatelessWidget { + const SpaceDetailsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const Dialog( + child: Text('Create Space'), + ); + } +} From 2a2fb7ffca48658623ec6f366ac9b97c61b9892f Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 11:36:50 +0300 Subject: [PATCH 25/75] Add responsive input fields and radio groups for visitor password setup --- .../view/access_type_radio_group.dart | 120 ++++ .../view/responsive_fields_row.dart | 73 +++ .../view/usage_frequency_radio_group.dart | 91 +++ .../view/visitor_password_dialog.dart | 589 +++++++++--------- 4 files changed, 574 insertions(+), 299 deletions(-) create mode 100644 lib/pages/visitor_password/view/access_type_radio_group.dart create mode 100644 lib/pages/visitor_password/view/responsive_fields_row.dart create mode 100644 lib/pages/visitor_password/view/usage_frequency_radio_group.dart diff --git a/lib/pages/visitor_password/view/access_type_radio_group.dart b/lib/pages/visitor_password/view/access_type_radio_group.dart new file mode 100644 index 00000000..be4adb9d --- /dev/null +++ b/lib/pages/visitor_password/view/access_type_radio_group.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; + +class AccessTypeRadioGroup extends StatelessWidget { + final String? selectedType; + final String? accessTypeSelected; + final Function(String) onTypeSelected; + final VisitorPasswordBloc visitorBloc; + + const AccessTypeRadioGroup({ + super.key, + required this.selectedType, + required this.accessTypeSelected, + required this.onTypeSelected, + required this.visitorBloc, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + Text('Access Type', style: text), + ], + ), + const SizedBox(height: 8), + if (size.width < 800) + Column( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + ], + ) + else + Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + ], + ), + ), + const Spacer(flex: 2), + ], + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + }) { + return SizedBox( + width: width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black, + fontSize: 13, + )), + value: value, + groupValue: groupValue, + onChanged: (value) { + if (value != null) { + onChanged(value); + if (value == 'Dynamic Password') { + visitorBloc.usageFrequencySelected = ''; + } + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/responsive_fields_row.dart b/lib/pages/visitor_password/view/responsive_fields_row.dart new file mode 100644 index 00000000..92a79276 --- /dev/null +++ b/lib/pages/visitor_password/view/responsive_fields_row.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; + +class NameAndEmailFields extends StatelessWidget { + final TextEditingController nameController; + final TextEditingController emailController; + final String? Function(String?)? nameValidator; + final String? Function(String?)? emailValidator; + + const NameAndEmailFields({ + super.key, + required this.nameController, + required this.emailController, + required this.nameValidator, + required this.emailValidator, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return Container( + width: size.width, + child: size.width < 800 + ? Column( + children: [ + CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + const SizedBox(height: 15), + CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ], + ) + : Row( + children: [ + Expanded( + flex: 2, + child: CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ), + const Spacer(), + ], + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/usage_frequency_radio_group.dart b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart new file mode 100644 index 00000000..aebebefe --- /dev/null +++ b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class UsageFrequencyRadioGroup extends StatelessWidget { + final String? selectedFrequency; + final String? usageFrequencySelected; + final Function(String) onFrequencySelected; + + const UsageFrequencyRadioGroup({ + super.key, + required this.selectedFrequency, + required this.usageFrequencySelected, + required this.onFrequencySelected, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return size.width < 600 + ? Column( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + ], + ) + : Row( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + required TextStyle text, + bool fullWidth = false, + }) { + return SizedBox( + width: fullWidth ? double.infinity : width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, style: text), + value: value, + groupValue: groupValue, + onChanged: (String? value) { + if (value != null) { + onChanged(value); + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 1e43af46..4b5cb0e2 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -9,8 +9,11 @@ import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/access_type_radio_group.dart'; import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart'; +import 'package:syncrow_web/pages/visitor_password/view/responsive_fields_row.dart'; +import 'package:syncrow_web/pages/visitor_password/view/usage_frequency_radio_group.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -21,7 +24,10 @@ class VisitorPasswordDialog extends StatelessWidget { @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; - var text = Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black, fontSize: 13); + var text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); return BlocProvider( create: (context) => VisitorPasswordBloc(), child: BlocListener( @@ -35,7 +41,8 @@ class VisitorPasswordDialog extends StatelessWidget { title: 'Sent Successfully', widgeta: Column( children: [ - if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.failedOperations.isNotEmpty) Column( children: [ const Text('Failed Devices'), @@ -45,7 +52,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.failedOperations.length, + itemCount: visitorBloc + .passwordStatus!.failedOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), @@ -53,14 +61,17 @@ class VisitorPasswordDialog extends StatelessWidget { height: 45, child: Center( child: Text(visitorBloc - .passwordStatus!.failedOperations[index].deviceUuid)), + .passwordStatus! + .failedOperations[index] + .deviceUuid)), ); }, ), ), ], ), - if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.successOperations.isNotEmpty) Column( children: [ const Text('Success Devices'), @@ -70,15 +81,18 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.successOperations.length, + itemCount: visitorBloc + .passwordStatus!.successOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc.passwordStatus! - .successOperations[index].deviceUuid)), + child: Text(visitorBloc + .passwordStatus! + .successOperations[index] + .deviceUuid)), ); }, ), @@ -89,7 +103,6 @@ class VisitorPasswordDialog extends StatelessWidget { )) .then((v) { Navigator.of(context).pop(true); - }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -102,15 +115,16 @@ class VisitorPasswordDialog extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, VisitorPasswordState state) { final visitorBloc = BlocProvider.of(context); - bool isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; + bool isRepeat = + state is IsRepeatState ? state.repeat : visitorBloc.repeat; return AlertDialog( backgroundColor: Colors.white, title: Text( 'Create visitor password', - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith(fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 24, + color: Colors.black), ), content: state is LoadingInitialState ? const Center(child: CircularProgressIndicator()) @@ -121,34 +135,11 @@ class VisitorPasswordDialog extends StatelessWidget { padding: const EdgeInsets.all(5.0), child: ListBody( children: [ - Container( - child: Row( - children: [ - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validate, - controller: visitorBloc.userNameController, - isRequired: true, - textFieldName: 'Name', - description: '', - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validateEmail, - controller: visitorBloc.emailController, - isRequired: true, - textFieldName: 'Email Address', - description: - 'The password will be sent to the visitor’s email address.', - ), - ), - const Spacer(), - ], - ), + NameAndEmailFields( + nameController: visitorBloc.userNameController, + emailController: visitorBloc.emailController, + nameValidator: visitorBloc.validate, + emailValidator: visitorBloc.validateEmail, ), const SizedBox( height: 15, @@ -156,107 +147,43 @@ class VisitorPasswordDialog extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text('Access Type', style: text), - ], + AccessTypeRadioGroup( + selectedType: state is PasswordTypeSelected + ? state.selectedType + : null, + accessTypeSelected: + visitorBloc.accessTypeSelected, + onTypeSelected: (value) { + context + .read() + .add(SelectPasswordType(value)); + }, + visitorBloc: visitorBloc, ), - Row( - children: [ - Expanded( - flex: 2, - child: Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Online Password', - style: text, - ), - value: 'Online Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Offline Password', style: text), - value: 'Offline Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - // SizedBox( - // width: size.width * 0.12, - // child: RadioListTile( - // contentPadding: EdgeInsets.zero, - // title: Text( - // 'Dynamic Password', - // style: text, - // ), - // value: 'Dynamic Password', - // groupValue: (state is PasswordTypeSelected) - // ? state.selectedType - // : visitorBloc.accessTypeSelected, - // onChanged: (String? value) { - // if (value != null) { - // context - // .read() - // .add(SelectPasswordType(value)); - // visitorBloc.usageFrequencySelected = ''; - // } - // }, - // ), - // ), - ], - )), - const Spacer( - flex: 2, - ), - ], - ), - if (visitorBloc.accessTypeSelected == 'Online Password') + + if (visitorBloc.accessTypeSelected == + 'Online Password') Text( 'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), - if (visitorBloc.accessTypeSelected == 'Offline Password') + if (visitorBloc.accessTypeSelected == + 'Offline Password') Text( 'Unaffected by the online status of the device, you can select online or offline device, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), // if (visitorBloc.accessTypeSelected == 'Dynamic Password') // Text( @@ -271,143 +198,170 @@ class VisitorPasswordDialog extends StatelessWidget { ) ], ), - visitorBloc.accessTypeSelected == 'Dynamic Password' - ? const SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (visitorBloc.accessTypeSelected == + 'Dynamic Password') + const SizedBox() + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text( - 'Usage Frequency', - style: text, - ), - ], + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), ), - Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'One-Time', - style: text, - ), - value: 'One-Time', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Periodic', style: text), - value: 'Periodic', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context.read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - ], + Text( + 'Usage Frequency', + style: text, ), - - //One-Time - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - // Periodic - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), ], ), + UsageFrequencyRadioGroup( + selectedFrequency: + state is UsageFrequencySelected + ? state.selectedFrequency + : null, + usageFrequencySelected: + visitorBloc.usageFrequencySelected, + onFrequencySelected: (value) { + context + .read() + .add(SelectUsageFrequency(value)); + }, + ), + + //One-Time + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, each device can be unlocked only once.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + // Periodic + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + ], + ), const SizedBox( height: 20, ), - if ((visitorBloc.usageFrequencySelected != 'One-Time' || - visitorBloc.accessTypeSelected != 'Offline Password') && + if ((visitorBloc.usageFrequencySelected != + 'One-Time' || + visitorBloc.accessTypeSelected != + 'Offline Password') && (visitorBloc.usageFrequencySelected != '')) DateTimeWebWidget( isRequired: true, title: 'Access Period', size: size, endTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add(SelectTimeEvent(context: context, isEffective: false)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: false)); } else { - visitorBloc.add(SelectTimeVisitorPassword(context: context, isStart: false, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); } }, startTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add( - SelectTimeEvent(context: context, isEffective: true)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: true)); } else { - visitorBloc.add(SelectTimeVisitorPassword( - context: context, isStart: true, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: true, + isRepeat: false)); } }, - firstString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + firstString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.effectiveTime - : visitorBloc.startTimeAccess.toString(), - secondString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + : visitorBloc.startTimeAccess + .toString(), + secondString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.expirationTime : visitorBloc.endTimeAccess.toString(), icon: Assets.calendarIcon), - const SizedBox(height: 10,), - Text(visitorBloc.accessPeriodValidate, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.red),), + const SizedBox( + height: 10, + ), + Text( + visitorBloc.accessPeriodValidate, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.red), + ), const SizedBox( height: 20, ), @@ -431,16 +385,21 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), const SizedBox( height: 20, ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') SizedBox( width: 100, child: Column( @@ -451,7 +410,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: CupertinoSwitch( value: visitorBloc.repeat, onChanged: (value) { - visitorBloc.add(ToggleRepeatEvent()); + visitorBloc + .add(ToggleRepeatEvent()); }, applyTheme: true, ), @@ -459,12 +419,16 @@ class VisitorPasswordDialog extends StatelessWidget { ], ), ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - isRepeat ? const RepeatWidget() : const SizedBox(), + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + isRepeat + ? const RepeatWidget() + : const SizedBox(), Container( decoration: containerDecoration, - width: size.width / 9, + width: size.width / 6, child: DefaultButton( onPressed: () { showDialog( @@ -472,22 +436,28 @@ class VisitorPasswordDialog extends StatelessWidget { barrierDismissible: false, builder: (BuildContext context) { return AddDeviceDialog( - selectedDeviceIds: visitorBloc.selectedDevices, + selectedDeviceIds: + visitorBloc.selectedDevices, ); }, ).then((listDevice) { if (listDevice != null) { - visitorBloc.selectedDevices = listDevice; + visitorBloc.selectedDevices = + listDevice; } }); }, borderRadius: 8, child: Text( '+ Add Device', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.whiteColors, - fontSize: 12), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: + ColorsManager.whiteColors, + fontSize: 12), ), ), ), @@ -525,30 +495,37 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { if (visitorBloc.forgetFormKey.currentState!.validate()) { if (visitorBloc.selectedDevices.isNotEmpty) { - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { setPasswordFunction(context, size, visitorBloc); - } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { if (visitorBloc.expirationTime != 'End Time' && - visitorBloc.effectiveTime != 'Start Time' ) { + visitorBloc.effectiveTime != 'Start Time') { setPasswordFunction(context, size, visitorBloc); - }else{ + } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - } else if( - visitorBloc.endTimeAccess.toString()!='End Time' - &&visitorBloc.startTimeAccess.toString()!='Start Time') { + } else if (visitorBloc.endTimeAccess.toString() != + 'End Time' && + visitorBloc.startTimeAccess.toString() != + 'Start Time') { if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { if (isRepeat == true) { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time' && visitorBloc.selectedDays.isNotEmpty) { - setPasswordFunction(context, size, visitorBloc); + setPasswordFunction( + context, size, visitorBloc); } else { visitorBloc.stateDialog( context: context, @@ -562,14 +539,16 @@ class VisitorPasswordDialog extends StatelessWidget { } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - }else{ - visitorBloc.stateDialog( - context: context, - message: 'Please select Access Period to continue', - title: 'Access Period'); + } else { + visitorBloc.stateDialog( + context: context, + message: + 'Please select Access Period to continue', + title: 'Access Period'); } } else { visitorBloc.stateDialog( @@ -615,7 +594,8 @@ class VisitorPasswordDialog extends StatelessWidget { content: SizedBox( height: size.height * 0.25, child: Center( - child: CircularProgressIndicator(), // Display a loading spinner + child: + CircularProgressIndicator(), // Display a loading spinner ), ), ); @@ -639,7 +619,10 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Set Password', - style: Theme.of(context).textTheme.headlineLarge!.copyWith( + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith( fontSize: 30, fontWeight: FontWeight.w400, color: Colors.black, @@ -689,37 +672,45 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { Navigator.pop(context); if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); - } - else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); } }, From 7e5825de45aa610348c91e45e94b11aac7284758 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:44:45 +0300 Subject: [PATCH 26/75] Fixed typo in occupancy sidebar. --- .../modules/occupancy/widgets/occupancy_end_side_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 3dd01bee..555841ca 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -23,7 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AnalyticsSidebarHeader(title: 'Presnce Sensor'), + const AnalyticsSidebarHeader(title: 'Presence Sensor'), Expanded( child: SizedBox( // height: MediaQuery.sizeOf(context).height * 0.2, From 6e6ef79ed0c8f9c99cbdb3be66577e50305c7f4d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:44:56 +0300 Subject: [PATCH 27/75] enhanced heat map tooltip's message. --- .../analytics/modules/occupancy/widgets/heat_map_tooltip.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart index c7695064..66612a3e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart @@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget { ), const Divider(height: 2, thickness: 1), Text( - '$value Occupants', + 'Occupancy detected: $value', style: context.textTheme.bodySmall?.copyWith( fontSize: 10, fontWeight: FontWeight.w500, From ee1ebeae2e904f06221bbc9411d9094a78f3718e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:45:15 +0300 Subject: [PATCH 28/75] Changed energy management charts titles for a more clear name. --- .../widgets/energy_consumption_per_device_chart_box.dart | 2 +- .../widgets/total_energy_consumption_chart_box.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index be5faf57..06b6c529 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( - title: Text('Energy Consumption per Device'), + title: Text('Device energy consumed'), ), ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index e197c297..4d88471d 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { child: FittedBox( alignment: AlignmentDirectional.centerStart, fit: BoxFit.scaleDown, - child: ChartTitle(title: Text('Total Energy Consumption')), + child: ChartTitle(title: Text('Space energy consumed')), ), ), const Spacer(flex: 4), From 010403f1fa83ad82432a6933c29631258f5d5ce8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:50:22 +0300 Subject: [PATCH 29/75] Added day of month axis name to all charts. --- .../widgets/aqi_distribution_chart.dart | 2 ++ .../energy_management_charts_helper.dart | 11 ++++----- .../occupancy/widgets/occupancy_chart.dart | 8 ++++--- .../widgets/charts_x_axis_title.dart | 23 +++++++++++++++++++ 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 lib/pages/analytics/widgets/charts_x_axis_title.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 2f3d7ff0..63e1d0d9 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -139,6 +140,7 @@ class AqiDistributionChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) => FittedBox( diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index b1af85c8..6b44e125 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper { return FlTitlesData( show: true, bottomTitles: AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), drawBelowEverything: true, sideTitles: SideTitles( interval: 1, @@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper { ); } - static String getToolTipLabel(num month, double value) { - final monthLabel = month.toString(); - final valueLabel = value.formatNumberToKwh; - final labels = [monthLabel, valueLabel]; - return labels.where((element) => element.isNotEmpty).join(', '); - } + static String getToolTipLabel(double value) => value.formatNumberToKwh; static List getTooltipItems(List touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( - getToolTipLabel(spot.x, spot.y), + getToolTipLabel(spot.y), const TextStyle( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w600, diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 70087c46..1205a66e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget { }) { final data = chartData; - final occupancyValue = double.parse(data[group.x.toInt()].occupancy); - final percentage = '${(occupancyValue).toStringAsFixed(0)}%'; + final occupancyValue = double.parse(data[group.x].occupancy); + final percentage = '${occupancyValue.toStringAsFixed(0)}%'; return BarTooltipItem( percentage, @@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget { alignment: AlignmentDirectional.centerStart, fit: BoxFit.scaleDown, child: Text( - '${(value).toStringAsFixed(0)}%', + '${value.toStringAsFixed(0)}%', style: context.textTheme.bodySmall?.copyWith( fontSize: 12, color: ColorsManager.greyColor, @@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) => FittedBox( diff --git a/lib/pages/analytics/widgets/charts_x_axis_title.dart b/lib/pages/analytics/widgets/charts_x_axis_title.dart new file mode 100644 index 00000000..746a8cbb --- /dev/null +++ b/lib/pages/analytics/widgets/charts_x_axis_title.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ChartsXAxisTitle extends StatelessWidget { + const ChartsXAxisTitle({ + this.label = 'Day of month', + super.key, + }); + + final String label; + + @override + Widget build(BuildContext context) { + return Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ); + } +} From f901983aa57296c8339ee3a6a9eccb17c10c2eed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 15:01:25 +0300 Subject: [PATCH 30/75] Implemented ranges for the values in the AQI chart based on the selected pollutant. --- .../helpers/range_of_aqi_charts_helper.dart | 11 ++++++-- .../widgets/range_of_aqi_chart.dart | 28 +++++++++++++++++-- .../widgets/range_of_aqi_chart_box.dart | 7 ++++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 21cb2a9e..17b00506 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -18,7 +18,11 @@ abstract final class RangeOfAqiChartsHelper { (ColorsManager.hazardousPurple, 'Hazardous'), ]; - static FlTitlesData titlesData(BuildContext context, List data) { + static FlTitlesData titlesData( + BuildContext context, + List data, { + double leftSideInterval = 50, + }) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( @@ -38,10 +42,11 @@ abstract final class RangeOfAqiChartsHelper { leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 50, + interval: leftSideInterval, maxIncluded: false, + minIncluded: true, getTitlesWidget: (value, meta) { - final text = value >= 300 ? '301+' : value.toInt().toString(); + final text = value.toInt().toString(); return Padding( padding: const EdgeInsetsDirectional.only(end: 12), child: FittedBox( diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart index 5e731d90..0914eab3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RangeOfAqiChart extends StatelessWidget { final List chartData; + final AqiType selectedAqiType; const RangeOfAqiChart({ super.key, required this.chartData, + required this.selectedAqiType, }); List<(List values, Color color, Color? dotColor)> get _lines { @@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget { ]; } + (double maxY, double interval) get _maxYForAqiType { + const aqiMaxValues = { + AqiType.aqi: (401, 100), + AqiType.pm25: (351, 50), + AqiType.pm10: (501, 100), + AqiType.hcho: (301, 50), + AqiType.tvoc: (501, 50), + AqiType.co2: (1251, 250), + }; + + return aqiMaxValues[selectedAqiType]!; + } + @override Widget build(BuildContext context) { return LineChart( LineChartData( minY: 0, - maxY: 301, + maxY: _maxYForAqiType.$1, clipData: const FlClipData.vertical(), - gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: _maxYForAqiType.$2, + ), + titlesData: RangeOfAqiChartsHelper.titlesData( + context, + chartData, + leftSideInterval: _maxYForAqiType.$2, + ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), betweenBarsData: [ diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index 6548c696..cb189dce 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), + Expanded( + child: RangeOfAqiChart( + chartData: state.filteredRangeOfAqi, + selectedAqiType: state.selectedAqiType, + ), + ), ], ), ); From 277a9ce4f007e472571c10fcbb909f837620f892 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 15:38:16 +0300 Subject: [PATCH 31/75] Add countdown seconds to schedule management --- .../schedule_device/bloc/schedule_bloc.dart | 14 +++- .../schedule_device/bloc/schedule_event.dart | 4 +- .../schedule_device/bloc/schedule_state.dart | 13 +++- .../count_down_inching_view.dart | 66 +++++++++++++++++-- .../schedule_widgets/schedual_view.dart | 4 +- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 62213205..fbf7ae64 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc { emit(currentState.copyWith( scheduleMode: event.scheduleMode, countdownRemaining: Duration.zero, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, )); } } @@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; emit(currentState.copyWith( + countdownSeconds: event.seconds, countdownHours: event.hours, countdownMinutes: event.minutes, inchingHours: 0, @@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc { inchingHours: event.hours, inchingMinutes: event.minutes, countdownRemaining: Duration.zero, + inchingSeconds: 0, // Add this )); } } @@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: countdownDuration.inMinutes % 60, countdownRemaining: countdownDuration, isCountdownActive: true, + countdownSeconds: countdownDuration.inSeconds, ), ); @@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: 0, countdownRemaining: Duration.zero, isCountdownActive: false, + countdownSeconds: 0, ), ); } @@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc { inchingMinutes: inchingDuration.inMinutes % 60, isInchingActive: true, countdownRemaining: inchingDuration, + countdownSeconds: inchingDuration.inSeconds, ), ); } @@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc { } String extractTime(String isoDateTime) { - // Example input: "2025-06-19T15:45:00.000" - return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00" + return isoDateTime.split('T')[1].split('.')[0]; } int? getTimeStampWithoutSeconds(DateTime? dateTime) { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 7ec144fe..0b9ec581 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -146,14 +146,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent { class UpdateCountdownTimeEvent extends ScheduleEvent { final int hours; final int minutes; + final int seconds; const UpdateCountdownTimeEvent({ required this.hours, required this.minutes, + required this.seconds, }); @override - List get props => [hours, minutes]; + List get props => [hours, minutes, seconds]; } class UpdateInchingTimeEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart index 10cd7611..63551c3a 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState { final bool isCountdownActive; final int inchingHours; final int inchingMinutes; + final int inchingSeconds; final bool isInchingActive; final ScheduleModes scheduleMode; final Duration? countdownRemaining; + final int? countdownSeconds; const ScheduleLoaded({ + this.countdownSeconds = 0, + this.inchingSeconds = 0, required this.schedules, this.selectedTime, required this.selectedDays, @@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState { bool? isInchingActive, ScheduleModes? scheduleMode, Duration? countdownRemaining, + String? deviceId, + int? countdownSeconds, + int? inchingSeconds, }) { return ScheduleLoaded( schedules: schedules ?? this.schedules, @@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState { selectedDays: selectedDays ?? this.selectedDays, functionOn: functionOn ?? this.functionOn, isEditing: isEditing ?? this.isEditing, - deviceId: deviceId, + deviceId: deviceId ?? this.deviceId, countdownHours: countdownHours ?? this.countdownHours, countdownMinutes: countdownMinutes ?? this.countdownMinutes, isCountdownActive: isCountdownActive ?? this.isCountdownActive, @@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive: isInchingActive ?? this.isInchingActive, scheduleMode: scheduleMode ?? this.scheduleMode, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + countdownSeconds: countdownSeconds ?? this.countdownSeconds, + inchingSeconds: inchingSeconds ?? this.inchingSeconds, ); } @@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive, scheduleMode, countdownRemaining, + countdownSeconds, + inchingSeconds, ]; } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart index d45073ec..418bab6c 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CountdownInchingView extends StatefulWidget { - const CountdownInchingView({super.key}); + final String deviceId; + const CountdownInchingView({super.key, required this.deviceId}); @override State createState() => _CountdownInchingViewState(); @@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget { class _CountdownInchingViewState extends State { late FixedExtentScrollController _hoursController; late FixedExtentScrollController _minutesController; + late FixedExtentScrollController _secondsController; int _lastHours = -1; int _lastMinutes = -1; + int _lastSeconds = -1; @override void initState() { super.initState(); _hoursController = FixedExtentScrollController(); _minutesController = FixedExtentScrollController(); + _secondsController = FixedExtentScrollController(); } @override void dispose() { _hoursController.dispose(); _minutesController.dispose(); + _secondsController.dispose(); super.dispose(); } - void _updateControllers(int displayHours, int displayMinutes) { + void _updateControllers( + int displayHours, int displayMinutes, int displaySeconds) { if (_lastHours != displayHours) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_hoursController.hasClients) { @@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State { }); _lastMinutes = displayMinutes; } + // Update seconds controller + if (_lastSeconds != displaySeconds) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_secondsController.hasClients) { + _secondsController.jumpToItem(displaySeconds); + } + }); + _lastSeconds = displaySeconds; + } } @override @@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State { return BlocBuilder( builder: (context, state) { if (state is! ScheduleLoaded) return const SizedBox.shrink(); - final isCountDown = state.scheduleMode == ScheduleModes.countdown; final isActive = isCountDown ? state.isCountdownActive : state.isInchingActive; @@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State { final displayMinutes = isActive && state.countdownRemaining != null ? state.countdownRemaining!.inMinutes.remainder(60) : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + final displaySeconds = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inSeconds.remainder(60) + : (isCountDown ? state.countdownSeconds : state.inchingSeconds); + + _updateControllers(displayHours, displayMinutes, displaySeconds!); + + if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) { + context.read().add( + StopScheduleEvent( + mode: ScheduleModes.countdown, + deviceId: widget.deviceId, + ), + ); + } - _updateControllers(displayHours, displayMinutes); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: value, minutes: displayMinutes)); + hours: value, + minutes: displayMinutes, + seconds: displaySeconds, + )); } }, isActive: isActive, @@ -115,11 +145,35 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: displayHours, minutes: value)); + hours: displayHours, + minutes: value, + seconds: displaySeconds, + )); } }, isActive: isActive, ), + const SizedBox(width: 10), + if (isActive) + _buildPickerColumn( + context, + 's', + displaySeconds, + 60, + _secondsController, + (value) { + if (!isActive) { + context + .read() + .add(UpdateCountdownTimeEvent( + hours: displayHours, + minutes: displayMinutes, + seconds: value, + )); + } + }, + isActive: isActive, + ), ], ), ], diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 2fa34559..47534d37 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -74,7 +74,9 @@ class BuildScheduleView extends StatelessWidget { ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - const CountdownInchingView(), + CountdownInchingView( + deviceId: deviceUuid, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.countdown) CountdownModeButtons( From c6e98fa24550d5452494e7c23a4d7a06d7201ccd Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 16:06:53 +0300 Subject: [PATCH 32/75] Refactor visitor password dialog navigation to return specific values on pop --- .../visitor_password/view/visitor_password_dialog.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4b5cb0e2..978b425a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -102,7 +102,7 @@ class VisitorPasswordDialog extends StatelessWidget { ], )) .then((v) { - Navigator.of(context).pop(true); + Navigator.of(context).pop(v); }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -476,7 +476,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(true); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( @@ -651,7 +651,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( From c649044a1fc3c98422cf9ba1a8ef15bdf4e3c19e Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 16:40:42 +0300 Subject: [PATCH 33/75] Enhance navigation buttons in SmartPowerDeviceControl for better user experience --- .../view/smart_power_device_control.dart | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart index 67313802..11d1cc8f 100644 --- a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -12,7 +12,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; //Smart Power Clamp -class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout { +class SmartPowerDeviceControl extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const SmartPowerDeviceControl({super.key, required this.deviceId}); @@ -145,13 +146,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou children: [ IconButton( icon: const Icon(Icons.arrow_left), - onPressed: () { - blocProvider.add(SmartPowerArrowPressedEvent(-1)); - pageController.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + onPressed: blocProvider.currentPage <= 0 + ? null + : () { + blocProvider + .add(SmartPowerArrowPressedEvent(-1)); + pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, ), Text( currentPage == 0 @@ -165,13 +169,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou ), IconButton( icon: const Icon(Icons.arrow_right), - onPressed: () { - blocProvider.add(SmartPowerArrowPressedEvent(1)); - pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + onPressed: blocProvider.currentPage >= 3 + ? null + : () { + blocProvider + .add(SmartPowerArrowPressedEvent(1)); + pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, ), ], ), @@ -195,8 +202,8 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou blocProvider.add(SelectDateEvent(context: context)); blocProvider.add(FilterRecordsByDateEvent( selectedDate: blocProvider.dateTime!, - viewType: - blocProvider.views[blocProvider.currentIndex])); + viewType: blocProvider + .views[blocProvider.currentIndex])); }, widget: blocProvider.dateSwitcher(), chartData: blocProvider.energyDataList.isNotEmpty From 52b843d51429935fbd2cd4b5290b34804fe06575 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 09:53:09 +0300 Subject: [PATCH 34/75] SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when-selecting-space-from-sidebar. --- lib/pages/space_tree/bloc/space_tree_bloc.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index e8c2e015..7d1a4d96 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -289,7 +289,6 @@ class SpaceTreeBloc extends Bloc { selectedSpaces: updatedSelectedSpaces, soldCheck: updatedSoldChecks, selectedCommunityAndSpaces: communityAndSpaces)); - emit(state.copyWith(selectedSpaces: updatedSelectedSpaces)); } catch (e) { emit(const SpaceTreeErrorState('Something went wrong')); } @@ -445,10 +444,12 @@ class SpaceTreeBloc extends Bloc { List _getThePathToChild(String communityId, String selectedSpaceId) { List ids = []; - for (var community in state.communityList) { + final communityDataSource = + state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; + for (final community in communityDataSource) { if (community.uuid == communityId) { - for (var space in community.spaces) { - List list = []; + for (final space in community.spaces) { + final list = []; list.add(space.uuid!); ids = _getAllParentsIds(space, selectedSpaceId, List.from(list)); if (ids.isNotEmpty) { From 562c67a958dbb3526ba677e4fc8f129a0b01af69 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 12:24:09 +0300 Subject: [PATCH 35/75] Add deviceName field to FailedOperation and SuccessOperation models --- .../visitor_password/model/failed_operation.dart | 13 ++++++++----- .../view/visitor_password_dialog.dart | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pages/visitor_password/model/failed_operation.dart b/lib/pages/visitor_password/model/failed_operation.dart index 223f9ac5..120f6d89 100644 --- a/lib/pages/visitor_password/model/failed_operation.dart +++ b/lib/pages/visitor_password/model/failed_operation.dart @@ -2,11 +2,13 @@ class FailedOperation { final bool success; final dynamic deviceUuid; final dynamic error; + final String deviceName; FailedOperation({ required this.success, required this.deviceUuid, required this.error, + required this.deviceName, }); factory FailedOperation.fromJson(Map json) { @@ -14,6 +16,7 @@ class FailedOperation { success: json['success'], deviceUuid: json['deviceUuid'], error: json['error'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -22,21 +25,22 @@ class FailedOperation { 'success': success, 'deviceUuid': deviceUuid, 'error': error, + 'deviceName': deviceName, }; } } - - class SuccessOperation { final bool success; // final Result result; final String deviceUuid; + final String deviceName; SuccessOperation({ required this.success, // required this.result, required this.deviceUuid, + required this.deviceName, }); factory SuccessOperation.fromJson(Map json) { @@ -44,6 +48,7 @@ class SuccessOperation { success: json['success'], // result: Result.fromJson(json['result']), deviceUuid: json['deviceUuid'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -52,6 +57,7 @@ class SuccessOperation { 'success': success, // 'result': result.toJson(), 'deviceUuid': deviceUuid, + 'deviceName': deviceName, }; } } @@ -92,8 +98,6 @@ class SuccessOperation { // } // } - - class PasswordStatus { final List successOperations; final List failedOperations; @@ -121,4 +125,3 @@ class PasswordStatus { }; } } - diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 978b425a..d1fb172a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: Text(visitorBloc .passwordStatus! .failedOperations[index] - .deviceUuid)), + .deviceName)), ); }, ), @@ -92,7 +92,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: Text(visitorBloc .passwordStatus! .successOperations[index] - .deviceUuid)), + .deviceName)), ); }, ), From 7397486e7ac3c948cc5e3de1e40b9267c3e25509 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:11:49 +0300 Subject: [PATCH 36/75] SP-368-Clarification-on-Default-Value-for-Start-Date-in-Door-Lock-Online-Tile-Limited-Password-repeat-section --- .../bloc/visitor_password_bloc.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 438b1abf..6dc20cfd 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -68,7 +68,7 @@ class VisitorPasswordBloc DateTime? startTime = DateTime.now(); DateTime? endTime; - String startTimeAccess = 'Start Time'; + String startTimeAccess = DateTime.now().toString().split('.').first; String endTimeAccess = 'End Time'; PasswordStatus? passwordStatus; selectAccessType( @@ -136,6 +136,27 @@ class VisitorPasswordBloc ); return; } + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text('Effective Time cannot be earlier than current time.'), + actionsAlignment: MainAxisAlignment.center, + content: + FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false)); + }, + child: const Text('OK'), + ), + + ), + ); + } + return; + } effectiveTimeTimeStamp = selectedTimestamp; startTimeAccess = selectedDateTime.toString().split('.').first; } else { From 5f5958369647da17bb4e346a56cae85bdcdd18f8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:12 +0300 Subject: [PATCH 37/75] Supported `NCPS` device type in occupancy devices dropdown. --- .../modules/occupancy/helpers/fetch_occupancy_data_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart index 0b01fda2..3bd96bce 100644 --- a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart +++ b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart @@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper { param: GetAnalyticsDevicesParam( communityUuid: communityUuid, spaceUuid: spaceUuid, - deviceTypes: ['WPS', 'CPS'], + deviceTypes: ['WPS', 'CPS', 'NCPS'], requestType: AnalyticsDeviceRequestType.occupancy, ), onSuccess: (device) { From 3b4952db0ae650b0af5aac028bc0ffb26ec3d238 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:30 +0300 Subject: [PATCH 38/75] fixed thrown exceptions in`AnalyticsDevice`. --- lib/pages/analytics/models/analytics_device.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 3340a41d..869de23f 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -25,8 +25,8 @@ class AnalyticsDevice { factory AnalyticsDevice.fromJson(Map json) { return AnalyticsDevice( - uuid: json['uuid'] as String, - name: json['name'] as String, + uuid: json['uuid'] as String? ?? '', + name: json['name'] as String? ?? '', createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, @@ -39,8 +39,8 @@ class AnalyticsDevice { ? ProductDevice.fromJson(json['productDevice'] as Map) : null, spaceUuid: json['spaceUuid'] as String?, - latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null, - longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null, + latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null, + longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null, ); } } From 6d667af7dcf099b48709cbbc7618cc067358e7ef Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:46 +0300 Subject: [PATCH 39/75] increased size of `OccupancyEndSideBar` in medium sized screens. --- .../modules/occupancy/views/analytics_occupancy_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart index 3a025254..56f8ce08 100644 --- a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart +++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart @@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget { child: Column( spacing: 32, children: [ - SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()), + SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()), SizedBox(height: height * 0.5, child: const OccupancyChartBox()), SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()), ], From 22070ca04a385817093b90880f8ddea48fba1539 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:26:07 +0300 Subject: [PATCH 40/75] removed unused comment. --- .../modules/occupancy/widgets/occupancy_end_side_bar.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 3dd01bee..75455a9b 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -26,7 +26,6 @@ class OccupancyEndSideBar extends StatelessWidget { const AnalyticsSidebarHeader(title: 'Presnce Sensor'), Expanded( child: SizedBox( - // height: MediaQuery.sizeOf(context).height * 0.2, child: PowerClampEnergyStatusWidget( status: [ PowerClampEnergyStatus( From 5e0df09cb6be5c8c8807a40a5cede8518cc2135f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:25:53 +0300 Subject: [PATCH 41/75] Changed tvoc unit to match the device. --- .../modules/air_quality/widgets/aqi_type_dropdown.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 5d482d9c..457bf610 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -7,7 +7,7 @@ enum AqiType { pm25('PM2.5', 'µg/m³', 'pm25'), pm10('PM10', 'µg/m³', 'pm10'), hcho('HCHO', 'mg/m³', 'cho2'), - tvoc('TVOC', 'µg/m³', 'voc'), + tvoc('TVOC', 'mg/m³', 'voc'), co2('CO2', 'ppm', 'co2'); const AqiType(this.value, this.unit, this.code); From e1bb67d7bd8074ffe2d0adb6e21d72cd0c7dfe4e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:26:05 +0300 Subject: [PATCH 42/75] reads correct value for TVOC. --- .../analytics/modules/air_quality/widgets/aqi_device_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index ebe88614..23ae874e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget { ); final tvocValue = _getValueForStatus( status, - 'tvoc_value', + 'voc_value', formatter: (value) => (value / 100).toStringAsFixed(2), ); From 520b73717ad886d3179179cedd3aa91d83c355bc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:34:07 +0300 Subject: [PATCH 43/75] Doesnt load devices when date changes. --- .../air_quality/helpers/fetch_air_quality_data_helper.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 223c0357..d7fbc279 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -24,11 +24,13 @@ abstract final class FetchAirQualityDataHelper { }) { final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; + if (shouldFetchAnalyticsDevices) { loadAnalyticsDevices( context, communityUuid: communityUuid, spaceUuid: spaceUuid, ); + } loadRangeOfAqi( context, spaceUuid: spaceUuid, From 30e940fdfc2fda32205c76ea0f4735ab28ea829f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:34:23 +0300 Subject: [PATCH 44/75] Reads the correct date to load aqi data. --- .../helpers/fetch_air_quality_data_helper.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index d7fbc279..5c63e397 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_qualit import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; -import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; @@ -22,14 +21,13 @@ abstract final class FetchAirQualityDataHelper { required String spaceUuid, bool shouldFetchAnalyticsDevices = true, }) { - final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; if (shouldFetchAnalyticsDevices) { - loadAnalyticsDevices( - context, - communityUuid: communityUuid, - spaceUuid: spaceUuid, - ); + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); } loadRangeOfAqi( context, From f38ac58442deb9172cdc702ee2dde1983749a0cb Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 14:45:10 +0300 Subject: [PATCH 45/75] Add bloc closure handling and improve device status updates in AcBloc --- .../device_managment/ac/bloc/ac_bloc.dart | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index af5a7b0a..eaababe1 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -16,6 +16,7 @@ class AcBloc extends Bloc { final ControlDeviceService controlDeviceService; final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; + bool _isBlocClosed = false; AcBloc({ required this.deviceId, @@ -45,7 +46,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -68,12 +70,13 @@ class AcBloc extends Bloc { } } - void _listenToChanges(deviceId) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; - - stream.listen((DatabaseEvent event) async { + _deviceStatusSubscription = + ref.onValue.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; Map usersMap = @@ -82,11 +85,15 @@ class AcBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); + statusList + .add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { + deviceStatus = + AcStatusModel.fromJson(usersMap['productUuid'], statusList); + print('Device status updated: ${deviceStatus.acSwitch}'); + + if (!_isBlocClosed) { add(AcStatusUpdated(deviceStatus)); } }); @@ -106,15 +113,14 @@ class AcBloc extends Bloc { Emitter emit, ) async { emit(AcsLoadingState()); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); try { final success = await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); if (!success) { emit(const AcsFailedState(error: 'Failed to control device')); } @@ -129,8 +135,10 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -293,13 +301,17 @@ class AcBloc extends Bloc { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - add(UpdateTimerEvent()); + if (!_isBlocClosed) { + add(UpdateTimerEvent()); + } } else { _countdownTimer?.cancel(); timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - add(TimerCompletedEvent()); + if (!_isBlocClosed) { + add(TimerCompletedEvent()); + } } }); } @@ -326,7 +338,9 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - add(UpdateTimerEvent()); + if (!_isBlocClosed) { + add(UpdateTimerEvent()); + } } } @@ -370,6 +384,9 @@ class AcBloc extends Bloc { @override Future close() { add(OnClose()); + _countdownTimer?.cancel(); + _deviceStatusSubscription?.cancel(); + _isBlocClosed = true; return super.close(); } } From 3c9494963d60a12767f559d84629fc6eee356079 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 15:58:58 +0300 Subject: [PATCH 46/75] Add generated configuration files for Flutter integration across platforms --- lib/pages/device_managment/ac/bloc/ac_bloc.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index eaababe1..38d11a46 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -16,7 +16,6 @@ class AcBloc extends Bloc { final ControlDeviceService controlDeviceService; final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; - bool _isBlocClosed = false; AcBloc({ required this.deviceId, @@ -93,7 +92,7 @@ class AcBloc extends Bloc { AcStatusModel.fromJson(usersMap['productUuid'], statusList); print('Device status updated: ${deviceStatus.acSwitch}'); - if (!_isBlocClosed) { + if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } }); @@ -301,7 +300,7 @@ class AcBloc extends Bloc { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - if (!_isBlocClosed) { + if (!isClosed) { add(UpdateTimerEvent()); } } else { @@ -309,7 +308,7 @@ class AcBloc extends Bloc { timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - if (!_isBlocClosed) { + if (!isClosed) { add(TimerCompletedEvent()); } } @@ -338,7 +337,7 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - if (!_isBlocClosed) { + if (!isClosed) { add(UpdateTimerEvent()); } } @@ -386,7 +385,6 @@ class AcBloc extends Bloc { add(OnClose()); _countdownTimer?.cancel(); _deviceStatusSubscription?.cancel(); - _isBlocClosed = true; return super.close(); } } From 6c268754a928c086f9e649de8824ed5194266cab Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:26:57 +0300 Subject: [PATCH 47/75] add icons and the basic route to show curtain module --- assets/icons/close_curtain.svg | 8 ++ assets/icons/open_curtain.svg | 8 ++ assets/icons/pause_curtain.svg | 6 + assets/icons/reverse_arrows.svg | 10 ++ .../helper/route_controls_based_code.dart | 113 ++++++++++++++---- 5 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 assets/icons/close_curtain.svg create mode 100644 assets/icons/open_curtain.svg create mode 100644 assets/icons/pause_curtain.svg create mode 100644 assets/icons/reverse_arrows.svg diff --git a/assets/icons/close_curtain.svg b/assets/icons/close_curtain.svg new file mode 100644 index 00000000..53f9e03b --- /dev/null +++ b/assets/icons/close_curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/open_curtain.svg b/assets/icons/open_curtain.svg new file mode 100644 index 00000000..715773a5 --- /dev/null +++ b/assets/icons/open_curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/pause_curtain.svg b/assets/icons/pause_curtain.svg new file mode 100644 index 00000000..8f90ea4f --- /dev/null +++ b/assets/icons/pause_curtain.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/reverse_arrows.svg b/assets/icons/reverse_arrows.svg new file mode 100644 index 00000000..fe119c39 --- /dev/null +++ b/assets/icons/reverse_arrows.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index 5586a310..cc09260e 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_s import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_batch.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_items.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart'; @@ -18,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dar import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart'; import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart'; @@ -39,8 +42,6 @@ import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heate import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart'; -import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; - mixin RouteControlsBasedCode { Widget routeControlsWidgets({required AllDevicesModel device}) { switch (device.productType) { @@ -84,6 +85,10 @@ mixin RouteControlsBasedCode { return CurtainStatusControlsView( deviceId: device.uuid!, ); + case 'CUR_2': + return CurtainModuleItems( + deviceId: device.uuid!, + ); case 'AC': return AcDeviceControlsView(device: device); case 'WH': @@ -107,7 +112,7 @@ mixin RouteControlsBasedCode { case 'SOS': return SosDeviceControlsView(device: device); - case 'NCPS': + case 'NCPS': return FlushMountedPresenceSensorControlView(device: device); default: return const SizedBox(); @@ -132,76 +137,140 @@ mixin RouteControlsBasedCode { switch (devices.first.productType) { case '1G': return WallLightBatchControlView( - deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '1G') + .map((e) => e.uuid!) + .toList(), ); case '2G': return TwoGangBatchControlView( - deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '2G') + .map((e) => e.uuid!) + .toList(), ); case '3G': return LivingRoomBatchControlsView( - deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '3G') + .map((e) => e.uuid!) + .toList(), ); case '1GT': return OneGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '1GT') + .map((e) => e.uuid!) + .toList(), ); case '2GT': return TwoGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '2GT') + .map((e) => e.uuid!) + .toList(), ); case '3GT': return ThreeGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '3GT') + .map((e) => e.uuid!) + .toList(), ); case 'GW': return GatewayBatchControlView( - gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(), + gatewayIds: devices + .where((e) => e.productType == 'GW') + .map((e) => e.uuid!) + .toList(), ); case 'DL': return DoorLockBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'DL') + .map((e) => e.uuid!) + .toList()); case 'WPS': return WallSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'WPS') + .map((e) => e.uuid!) + .toList()); case 'CPS': return CeilingSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'CPS') + .map((e) => e.uuid!) + .toList(), ); case 'CUR': return CurtainBatchStatusView( - devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'CUR') + .map((e) => e.uuid!) + .toList(), + ); + case 'CUR_2': + return CurtainModuleBatchView( + devicesIds: devices + .where((e) => e.productType == 'AC') + .map((e) => e.uuid!) + .toList(), ); case 'AC': return AcDeviceBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'AC') + .map((e) => e.uuid!) + .toList()); case 'WH': return WaterHEaterBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'WH') + .map((e) => e.uuid!) + .toList(), ); case 'DS': return MainDoorSensorBatchView( - devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'DS') + .map((e) => e.uuid!) + .toList(), ); case 'GD': return GarageDoorBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'GD') + .map((e) => e.uuid!) + .toList(), ); case 'WL': return WaterLeakBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'WL') + .map((e) => e.uuid!) + .toList(), ); case 'PC': return PowerClampBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'PC') + .map((e) => e.uuid!) + .toList(), ); case 'SOS': return SOSBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'SOS') + .map((e) => e.uuid!) + .toList(), ); case 'NCPS': return FlushMountedPresenceSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'NCPS') + .map((e) => e.uuid!) + .toList(), ); default: return const SizedBox(); From 03ba50629427bf2c160b3cf7f63e7ee931a6d862 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:27:15 +0300 Subject: [PATCH 48/75] add bloc nd logic --- .../bloc/batch/curtain_module_batch_bloc.dart | 45 ++++ .../bloc/curtain_module_bloc.dart | 232 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart new file mode 100644 index 00000000..87dd53f7 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'curtain_module_batch_event.dart'; +part 'curtain_module_batch_state.dart'; + +class CurtainModuleBatchBloc + extends Bloc { + final ControlDeviceService controlDeviceService; + StreamSubscription? _firebaseSubscription; + + CurtainModuleBatchBloc(this.controlDeviceService) + : super(CurtainModuleBatchInitial()) { + on(_onFetchAcBatchStatus); + } + + Future _onFetchAcBatchStatus( + CutrainModuleFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleBatchLoadingState()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + status.status.forEach( + (element) => print( + 'this is code ${element.code} - this is value ${element.value}'), + ); + + emit( + CurtainModuleBatchLoadedState( + curtainModuleStatusModel: CurtainModuleStatusModel.fromJson({}), + ), + ); + } catch (e) { + emit(CurtainModuleBatchFailedState(error: e.toString())); + } + } +} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart new file mode 100644 index 00000000..d79380cf --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -0,0 +1,232 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'curtain_module_event.dart'; +part 'curtain_module_state.dart'; + +class CurtainModuleBloc extends Bloc { + final ControlDeviceService controlDeviceService; + StreamSubscription? _firebaseSubscription; + + CurtainModuleBloc(this.controlDeviceService) : super(CurtainModuleInitial()) { + on(_onFetchCurtainModuleStatusEvent); + on(_onSendCurtainPercentToApiEvent); + on(_onOpenCurtainEvent); + on(_onCloseCurtainEvent); + on(_onStopCurtainEvent); + on(_onChangeTimerControlEvent); + on(_onChageCurCalibrationEvent); + on(_onChangeElecMachineryModeEvent); + on(_onChangeControlBackEvent); + on(_onChangeControlBackModeEvent); + on(_onChangeCurtainModuleStatusEvent); + } + + Future _onFetchCurtainModuleStatusEvent( + FetchCurtainModuleStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final result = Map.fromEntries( + status.status.map((element) => MapEntry(element.code, element.value)), + ); + + emit(CurtainModuleStatusLoaded( + curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), + )); + Map statusMap = {}; + final ref = + FirebaseDatabase.instance.ref('device-status/${event.deviceId}'); + final stream = ref.onValue; + + stream.listen((DatabaseEvent DatabaseEvent) async { + if (DatabaseEvent.snapshot.value == null) return; + + Map usersMap = + DatabaseEvent.snapshot.value as Map; + + List statusList = []; + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + statusMap = { + for (final element in statusList) element.code: element.value, + }; + if (!isClosed) { + add( + ChangeCurtainModuleStatusEvent( + deviceId: event.deviceId, + status: CurtainModuleStatusModel.fromJson(statusMap), + ), + ); + } + }); + } + + Future _onChangeCurtainModuleStatusEvent( + ChangeCurtainModuleStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + emit(CurtainModuleStatusLoaded(curtainModuleStatus: event.status)); + } + + Future _onSendCurtainPercentToApiEvent( + SendCurtainPercentToApiEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: event.status, + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to send control command: $e')); + } + } + + Future _onOpenCurtainEvent( + OpenCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'open'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to open curtain: $e')); + } + } + + Future _onCloseCurtainEvent( + CloseCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'close'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to close curtain: $e')); + } + } + + Future _onStopCurtainEvent( + StopCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'stop'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to stop curtain: $e')); + } + } + + Future _onChangeTimerControlEvent( + ChangeTimerControlEvent event, + Emitter emit, + ) async { + try { + if (event.timControl < 10 || event.timControl > 120) { + emit(const CurtainModuleError( + message: 'Timer control value must be between 10 and 120')); + return; + } + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'tr_timecon', + value: event.timControl, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change timer control: $e')); + } + } + + Future _onChageCurCalibrationEvent( + CurCalibrationEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'cur_calibration', value: 'start'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to start calibration: $e')); + } + } + + Future _onChangeElecMachineryModeEvent( + ChangeElecMachineryModeEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'elec_machinery_mode', + value: event.elecMachineryMode, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change mode: $e')); + } + } + + Future _onChangeControlBackEvent( + ChangeControlBackEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'control_back', + value: event.controlBack, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change control back: $e')); + } + } + + Future _onChangeControlBackModeEvent( + ChangeControlBackModeEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'control_back_mode', + value: event.controlBackMode, + ), + ); + } catch (e) { + emit(CurtainModuleError( + message: 'Failed to change control back mode: $e')); + } + } + + @override + Future close() async { + await _firebaseSubscription?.cancel(); + return super.close(); + } +} From eee6a80c50aa58b467aceee07506bedb2f04f1d9 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:27:40 +0300 Subject: [PATCH 49/75] add events and states and models --- .../batch/curtain_module_batch_event.dart | 19 +++ .../batch/curtain_module_batch_state.dart | 28 ++++ .../bloc/curtain_module_event.dart | 132 ++++++++++++++++++ .../bloc/curtain_module_state.dart | 37 +++++ .../models/curtain_module_model.dart | 53 +++++++ 5 files changed, 269 insertions(+) create mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart create mode 100644 lib/pages/device_managment/curtain_module/models/curtain_module_model.dart diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart new file mode 100644 index 00000000..351773f9 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart @@ -0,0 +1,19 @@ +part of 'curtain_module_batch_bloc.dart'; + +sealed class CurtainModuleBatchEvent extends Equatable { + const CurtainModuleBatchEvent(); + + @override + List get props => []; +} + +class CutrainModuleFetchBatchStatusEvent extends CurtainModuleBatchEvent { + final List devicesIds; + + const CutrainModuleFetchBatchStatusEvent({ + required this.devicesIds, + }); + + @override + List get props => [devicesIds]; +} diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart new file mode 100644 index 00000000..bb8bb7d0 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart @@ -0,0 +1,28 @@ +part of 'curtain_module_batch_bloc.dart'; + +sealed class CurtainModuleBatchState extends Equatable { + const CurtainModuleBatchState(); + + @override + List get props => []; +} + +final class CurtainModuleBatchInitial extends CurtainModuleBatchState {} + +final class CurtainModuleBatchLoadingState extends CurtainModuleBatchState {} + +final class CurtainModuleBatchLoadedState extends CurtainModuleBatchState { + final CurtainModuleStatusModel curtainModuleStatusModel; + const CurtainModuleBatchLoadedState({ + required this.curtainModuleStatusModel, + }); +} + +final class CurtainModuleBatchFailedState extends CurtainModuleBatchState { + final String error; + + const CurtainModuleBatchFailedState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart new file mode 100644 index 00000000..6b0d89ae --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart @@ -0,0 +1,132 @@ +part of 'curtain_module_bloc.dart'; + +sealed class CurtainModuleEvent extends Equatable { + const CurtainModuleEvent(); + + @override + List get props => []; +} + +class FetchCurtainModuleStatusEvent extends CurtainModuleEvent { + final String deviceId; + const FetchCurtainModuleStatusEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class SendCurtainPercentToApiEvent extends CurtainModuleEvent { + final String deviceId; + final Status status; + + const SendCurtainPercentToApiEvent({ + required this.deviceId, + required this.status, + }); + + @override + List get props => [deviceId, status]; +} + +class OpenCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const OpenCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class CloseCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const CloseCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class StopCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const StopCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class ChangeTimerControlEvent extends CurtainModuleEvent { + final String deviceId; + final int timControl; + + const ChangeTimerControlEvent({ + required this.deviceId, + required this.timControl, + }); + + @override + List get props => [deviceId, timControl]; +} + +class CurCalibrationEvent extends CurtainModuleEvent { + final String deviceId; + + const CurCalibrationEvent({ + required this.deviceId, + }); + + @override + List get props => [deviceId]; +} + +class ChangeElecMachineryModeEvent extends CurtainModuleEvent { + final String deviceId; + final String elecMachineryMode; + + const ChangeElecMachineryModeEvent({ + required this.deviceId, + required this.elecMachineryMode, + }); + + @override + List get props => [deviceId, elecMachineryMode]; +} + +class ChangeControlBackEvent extends CurtainModuleEvent { + final String deviceId; + final String controlBack; + + const ChangeControlBackEvent({ + required this.deviceId, + required this.controlBack, + }); + + @override + List get props => [deviceId, controlBack]; +} + +class ChangeControlBackModeEvent extends CurtainModuleEvent { + final String deviceId; + final String controlBackMode; + + const ChangeControlBackModeEvent({ + required this.deviceId, + required this.controlBackMode, + }); + + @override + List get props => [deviceId, controlBackMode]; +} + +class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent { + final String deviceId; + final CurtainModuleStatusModel status; + + const ChangeCurtainModuleStatusEvent({ + required this.deviceId, + required this.status, + }); + + @override + List get props => [deviceId, status]; +} \ No newline at end of file diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart new file mode 100644 index 00000000..02ef9279 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart @@ -0,0 +1,37 @@ +part of 'curtain_module_bloc.dart'; + +sealed class CurtainModuleState extends Equatable { + const CurtainModuleState(); + + @override + List get props => []; +} + +class CurtainModuleInitial extends CurtainModuleState {} + +class CurtainModuleLoading extends CurtainModuleState {} + +class CurtainModuleError extends CurtainModuleState { + final String message; + const CurtainModuleError({required this.message}); + + @override + List get props => [message]; +} + +class CurtainModuleStatusLoaded extends CurtainModuleState { + final CurtainModuleStatusModel curtainModuleStatus; + + const CurtainModuleStatusLoaded({required this.curtainModuleStatus}); + + @override + List get props => [curtainModuleStatus]; +} +class CurtainModuleStatusUpdated extends CurtainModuleState { + final CurtainModuleStatusModel curtainModuleStatus; + + const CurtainModuleStatusUpdated({required this.curtainModuleStatus}); + + @override + List get props => [curtainModuleStatus]; +} diff --git a/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart b/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart new file mode 100644 index 00000000..0b6d23fb --- /dev/null +++ b/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart @@ -0,0 +1,53 @@ + +enum CurtainModuleControl { + open, + close, + stop, +} + +// enum CurtainControlBackMode { +// foward, +// backward, +// } + +class CurtainModuleStatusModel { + CurtainModuleControl control; + int percentControl; + String curCalibration; + // CurtainControlBackMode controlBackmode; + int trTimeControl; + String elecMachineryMode; + String controlBack; + CurtainModuleStatusModel({ + required this.control, + required this.percentControl, + required this.curCalibration, + // required this.controlBackmode, + required this.trTimeControl, + required this.controlBack, + required this.elecMachineryMode, + }); + factory CurtainModuleStatusModel.zero() => CurtainModuleStatusModel( + control: CurtainModuleControl.stop, + percentControl: 0, + // controlBackmode: CurtainControlBackMode.foward, + curCalibration: '', + trTimeControl: 0, + controlBack: '', + elecMachineryMode: '', + ); + + factory CurtainModuleStatusModel.fromJson(Map json) { + return CurtainModuleStatusModel( + control: CurtainModuleControl.values.firstWhere( + (e) => e.toString() == json['control'] as String, + orElse: () => CurtainModuleControl.stop, + ), + percentControl: json['percent_control'] as int? ?? 0, + curCalibration: json['cur_calibration'] as String? ?? '', + trTimeControl: json['tr_timecon'] as int? ?? 0, + elecMachineryMode: json['elec_machinery_mode'] as String? ?? '', + controlBack: json['control_back'] as String? ?? '', + ); + } +} From 4e9bcbdcea3fab23c024d64196c7b2f069137bae Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:28:15 +0300 Subject: [PATCH 50/75] build UI and integrate with back --- .../view/curtain_module_batch.dart | 71 +++++++ .../view/curtain_module_items.dart | 98 +++++++++ .../widgets/accurate_calibrating_dialog.dart | 48 +++++ .../widgets/accurate_calibration_dialog.dart | 40 ++++ .../widgets/accurate_dialog_widget.dart | 97 +++++++++ .../widgets/calibrate_completed_dialog.dart | 77 +++++++ .../widgets/curtain_action_widget.dart | 41 ++++ .../widgets/curtain_movment_widget.dart | 190 ++++++++++++++++++ .../widgets/normal_text_body_for_dialog.dart | 42 ++++ .../widgets/number_input_textfield.dart | 27 +++ .../widgets/pref_revers_card_widget.dart | 79 ++++++++ .../widgets/prefrences_dialog.dart | 137 +++++++++++++ .../widgets/quick_calibrating_dialog.dart | 122 +++++++++++ .../widgets/quick_calibration_dialog.dart | 44 ++++ lib/utils/constants/assets.dart | 6 +- 15 files changed, 1118 insertions(+), 1 deletion(-) create mode 100644 lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart create mode 100644 lib/pages/device_managment/curtain_module/view/curtain_module_items.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart new file mode 100644 index 00000000..0e0dfdc3 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CurtainModuleBatchView extends StatelessWidget { + final List devicesIds; + const CurtainModuleBatchView({ + super.key, + required this.devicesIds, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainModuleBatchBloc(RemoteControlDeviceService()) + ..add(CutrainModuleFetchBatchStatusEvent(devicesIds: devicesIds)), + child: _buildStatusControls(context), + ); + } + + Widget _buildStatusControls(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ControlCurtainMovementWidget( + deviceId: devicesIds.first, + ), + const SizedBox( + height: 10, + ), + SizedBox( + height: 120, + width: 350, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: IconNameStatusContainer( + isFullIcon: false, + name: 'Factory Reset', + icon: Assets.factoryReset, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + ), + Expanded( + child: IconNameStatusContainer( + isFullIcon: false, + name: 'Firmware Update', + icon: Assets.firmware, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart new file mode 100644 index 00000000..95366e49 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + const CurtainModuleItems({ + super.key, + required this.deviceId, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainModuleBloc(RemoteControlDeviceService()) + ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), + child: _buildStatusControls(context), + ); + } + + Widget _buildStatusControls(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ControlCurtainMovementWidget( + deviceId: deviceId, + ), + const SizedBox( + height: 10, + ), + SizedBox( + height: 120, + width: 350, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: IconNameStatusContainer( + isFullIcon: false, + name: 'Schedules', + icon: Assets.schedule, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is CurtainModuleStatusLoaded) { + return IconNameStatusContainer( + isFullIcon: false, + name: 'Preferences', + icon: Assets.preferences, + onTap: () => showDialog( + context: context, + builder: (_) => BlocProvider.value( + value: context.watch(), + child: CurtainModulePrefrencesDialog( + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, + ), + ), + ), + status: false, + textColor: ColorsManager.blackColor, + ); + } else { + return const SizedBox(); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart new file mode 100644 index 00000000..54107420 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; + +class AccurteCalibratingDialog extends StatelessWidget { + final String deviceId; + final BuildContext parentContext; + const AccurteCalibratingDialog({ + super.key, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Calibrating', + body: const NormalTextBodyForDialog( + title: '', + step1: + '1. Click Close Button to make the Curtain run to Full Close and Position.', + step2: '2. click Next to complete the Calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + parentContext.read().add( + CurCalibrationEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => CalibrateCompletedDialog( + parentContext: parentContext, + deviceId: deviceId, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart new file mode 100644 index 00000000..a9d1b010 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; + +class AccurateCalibrationDialog extends StatelessWidget { + final String deviceId; + final BuildContext parentContext; + const AccurateCalibrationDialog({ + super.key, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Accurate Calibration', + body: const NormalTextBodyForDialog( + title: 'Prepare Calibration:', + step1: '1. Run The Curtain to the Fully Open Position,and pause.', + step2: '2. click Next to Start accurate calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => AccurteCalibratingDialog( + deviceId: deviceId, + parentContext: parentContext, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart new file mode 100644 index 00000000..5be376ae --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AccurateDialogWidget extends StatelessWidget { + final String title; + final Widget body; + final void Function() leftOnTap; + final void Function() rightOnTap; + const AccurateDialogWidget({ + super.key, + required this.title, + required this.body, + required this.leftOnTap, + required this.rightOnTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 300, + width: 400, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.blueColor, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + Padding( + padding: const EdgeInsets.all(10), + child: body, + ), + const SizedBox(height: 20), + const Spacer(), + const Divider(), + Row( + children: [ + Expanded( + child: InkWell( + onTap: leftOnTap, + child: Container( + height: 60, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.grayBorder), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: rightOnTap, + child: Container( + height: 60, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Next', + style: TextStyle( + color: ColorsManager.blueColor, + ), + ), + ), + ), + ) + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart new file mode 100644 index 00000000..9b2b5ea9 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CalibrateCompletedDialog extends StatelessWidget { + final BuildContext parentContext; + final String deviceId; + const CalibrateCompletedDialog({ + super.key, + required this.parentContext, + required this.deviceId, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: SizedBox( + height: 250, + width: 400, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Text( + 'Calibration Completed', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.blueColor, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + const Icon( + Icons.check_circle, + size: 100, + color: ColorsManager.blueColor, + ), + const Spacer(), + const Divider( + indent: 10, + endIndent: 10, + ), + InkWell( + onTap: () { + parentContext.read().add( + FetchCurtainModuleStatusEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + Navigator.of(parentContext).pop(); + }, + child: Container( + height: 40, + width: double.infinity, + alignment: Alignment.center, + child: const Text( + 'Close', + style: TextStyle( + color: ColorsManager.grayBorder, + ), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart new file mode 100644 index 00000000..8c2ff81c --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CurtainActionWidget extends StatelessWidget { + final String icon; + final void Function() onTap; + const CurtainActionWidget({ + super.key, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.whiteColors, + child: ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.graysColor, + child: SvgPicture.asset( + icon, + width: 35, + height: 35, + fit: BoxFit.contain, + ), + ), + ), + )), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart new file mode 100644 index 00000000..076c5a9c --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ControlCurtainMovementWidget extends StatelessWidget { + final String deviceId; + const ControlCurtainMovementWidget({ + super.key, + required this.deviceId, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 550, + child: DeviceControlsContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CurtainActionWidget( + icon: Assets.openCurtain, + onTap: () { + context.read().add( + OpenCurtainEvent(deviceId: deviceId), + ); + }, + ), + const SizedBox( + width: 30, + ), + CurtainActionWidget( + icon: Assets.pauseCurtain, + onTap: () { + context.read().add( + StopCurtainEvent(deviceId: deviceId), + ); + }, + ), + const SizedBox( + width: 30, + ), + CurtainActionWidget( + icon: Assets.closeCurtain, + onTap: () { + context.read().add( + CloseCurtainEvent(deviceId: deviceId), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleError) { + return Center( + child: Text( + state.message, + style: const TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } else if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator( + color: ColorsManager.minBlueDot, + ), + ); + } else if (state is CurtainModuleInitial) { + return const Center( + child: Text( + 'No data available', + style: TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } else if (state is CurtainModuleStatusLoaded) { + return CurtainSliderWidget( + status: state.curtainModuleStatus, + deviceId: deviceId, + ); + } else { + return const Center( + child: Text( + 'Unknown state', + style: TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } + }, + ) + ], + ), + ), + ); + } +} + +class CurtainSliderWidget extends StatefulWidget { + final CurtainModuleStatusModel status; + final String deviceId; + + const CurtainSliderWidget({ + super.key, + required this.status, + required this.deviceId, + }); + + @override + State createState() => _CurtainSliderWidgetState(); +} + +class _CurtainSliderWidgetState extends State { + double? _localValue; // For temporary drag state + + @override + Widget build(BuildContext context) { + // If user is dragging, use local value. Otherwise, use Firebase-synced state + final double currentSliderValue = + _localValue ?? widget.status.percentControl / 100; + + return Column( + children: [ + Text( + '${(currentSliderValue * 100).round()}%', + style: const TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + Slider( + value: currentSliderValue, + min: 0, + max: 1, + divisions: 10, // 10% step + activeColor: ColorsManager.minBlueDot, + thumbColor: ColorsManager.primaryColor, + inactiveColor: ColorsManager.whiteColors, + + // Start dragging — use local control + onChangeStart: (_) { + setState(() { + _localValue = currentSliderValue; + }); + }, + + // While dragging — update temporary value + onChanged: (value) { + final steppedValue = (value * 10).roundToDouble() / 10; + setState(() { + _localValue = steppedValue; + }); + }, + + // On release — send API and return to Firebase-controlled state + onChangeEnd: (value) { + final int targetPercent = (value * 100).round(); + + // Dispatch API call + context.read().add( + SendCurtainPercentToApiEvent( + deviceId: widget.deviceId, + status: Status( + code: 'percent_control', + value: targetPercent, + ), + ), + ); + + // Revert back to Firebase-synced stream + setState(() { + _localValue = null; + }); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart new file mode 100644 index 00000000..8818cb7b --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NormalTextBodyForDialog extends StatelessWidget { + final String title; + final String step1; + final String step2; + + const NormalTextBodyForDialog({ + super.key, + required this.title, + required this.step1, + required this.step2, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ), + Text( + step1, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ), + Text( + step2, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ) + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart new file mode 100644 index 00000000..ea95f838 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NumberInputField extends StatelessWidget { + final TextEditingController controller; + + const NumberInputField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: const TextStyle( + fontSize: 20, + color: ColorsManager.blackColor, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart new file mode 100644 index 00000000..81912e80 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class PrefReversCardWidget extends StatelessWidget { + final void Function() onTap; + final String title; + final String body; + const PrefReversCardWidget({ + super.key, + required this.title, + required this.body, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return DefaultContainer( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 8, + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayBorder, + fontSize: 15, + ), + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 2, + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(10), + right: Radius.circular(10)), + border: Border.all(color: ColorsManager.grayBorder)), + child: SvgPicture.asset( + Assets.reverseArrows, + height: 15, + ), + ), + ), + ) + ], + ), + SizedBox( + width: 100, + child: Text( + body, + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart new file mode 100644 index 00000000..c5fcf4ba --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class CurtainModulePrefrencesDialog extends StatelessWidget { + final CurtainModuleStatusModel curtainModuleStatusModel; + final String deviceId; + + const CurtainModulePrefrencesDialog({ + super.key, + required this.curtainModuleStatusModel, + required this.deviceId, + }); + + @override + Widget build(_) { + return AlertDialog( + backgroundColor: ColorsManager.CircleImageBackground, + contentPadding: const EdgeInsets.all(30), + title: const Center( + child: Text( + 'Preferences', + style: TextStyle( + color: ColorsManager.blueColor, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + )), + content: BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is CurtainModuleStatusLoaded) { + return SizedBox( + height: 300, + width: 400, + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.5, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + children: [ + PrefReversCardWidget( + title: state.curtainModuleStatus.controlBack, + body: 'Motor Steering', + onTap: () { + context.read().add( + ChangeControlBackEvent( + deviceId: deviceId, + controlBack: + state.curtainModuleStatus.controlBack == + 'forward' + ? 'back' + : 'forward', + ), + ); + }, + ), + PrefReversCardWidget( + title: state.curtainModuleStatus.elecMachineryMode, + body: 'Motor Mode', + onTap: () => context.read().add( + ChangeElecMachineryModeEvent( + deviceId: deviceId, + elecMachineryMode: + state.curtainModuleStatus.elecMachineryMode == + 'dry_contact' + ? 'strong_power' + : 'dry_contact', + ), + ), + ), + DefaultContainer( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => AccurateCalibrationDialog( + deviceId: deviceId, + parentContext: context, + ), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Accurte Calibration', + style: TextStyle( + fontSize: 18, + color: ColorsManager.blackColor, + )), + ], + ), + ), + ), + DefaultContainer( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => QuickCalibrationDialog( + timControl: state.curtainModuleStatus.trTimeControl, + deviceId: deviceId, + parentContext: context), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Quick Calibration', + style: TextStyle( + fontSize: 18, + color: ColorsManager.blackColor, + )), + ], + ), + ), + ), + ], + ), + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart new file mode 100644 index 00000000..0b86c96e --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/number_input_textfield.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class QuickCalibratingDialog extends StatefulWidget { + final int timControl; + final String deviceId; + final BuildContext parentContext; + const QuickCalibratingDialog({ + super.key, + required this.timControl, + required this.deviceId, + required this.parentContext, + }); + + @override + State createState() => _QuickCalibratingDialogState(); +} + +class _QuickCalibratingDialogState extends State { + late TextEditingController _controller; + String? _errorText; + + void _onRightTap() { + final value = int.tryParse(_controller.text); + + if (value == null || value < 10 || value > 120) { + setState(() { + _errorText = 'Number should be between 10 and 120'; + }); + return; + } + + setState(() { + _errorText = null; + }); + widget.parentContext.read().add( + ChangeTimerControlEvent( + deviceId: widget.deviceId, + timControl: value, + ), + ); + Navigator.of(widget.parentContext).pop(); + showDialog( + context: widget.parentContext, + builder: (_) => CalibrateCompletedDialog( + parentContext: widget.parentContext, + deviceId: widget.deviceId, + ), + ); + } + + @override + void initState() { + _controller = TextEditingController(text: widget.timControl.toString()); + super.initState(); + } + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Calibrating', + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '1. please Enter the Travel Time:', + style: TextStyle(color: ColorsManager.grayBorder), + ), + const SizedBox(height: 10), + Container( + width: 150, + height: 40, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: NumberInputField(controller: _controller), + ), + const Expanded( + child: Text( + 'seconds', + style: TextStyle( + fontSize: 15, + color: ColorsManager.blueColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + if (_errorText != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _errorText!, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 14, + ), + ), + ), + ], + ), + leftOnTap: () => Navigator.of(widget.parentContext).pop(), + rightOnTap: _onRightTap, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart new file mode 100644 index 00000000..803d904f --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart'; + +class QuickCalibrationDialog extends StatelessWidget { + final int timControl; + final String deviceId; + final BuildContext parentContext; + const QuickCalibrationDialog({ + super.key, + required this.timControl, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Quick Calibration', + body: const NormalTextBodyForDialog( + title: 'Prepare Calibration:', + step1: + '1. Confirm that the curtain is in the fully closed and suspended state.', + step2: '2. click Next to Start calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => QuickCalibratingDialog( + timControl: timControl, + deviceId: deviceId, + parentContext: parentContext, + ), + ); + }, + ), + ); + } +} diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index dfc0b394..331a4285 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -124,7 +124,11 @@ class Assets { //assets/icons/AC.svg static const String ac = "assets/icons/AC.svg"; //assets/icons/Curtain.svg - static const String curtain = "assets/icons/Curtain.svg"; + static const String curtain = 'assets/icons/Curtain.svg'; + static const String openCurtain = 'assets/icons/open_curtain.svg'; + static const String pauseCurtain = 'assets/icons/pause_curtain.svg'; + static const String closeCurtain = 'assets/icons/close_curtain.svg'; + static const String reverseArrows = 'assets/icons/reverse_arrows.svg'; //assets/icons/doorLock.svg static const String doorLock = "assets/icons/doorLock.svg"; //assets/icons/Gateway.svg From 0b372e1ed8746218c950cda7dc72521618510e0f Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:49:11 +0300 Subject: [PATCH 51/75] use read instead of watch --- .../curtain_module/view/curtain_module_items.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 95366e49..15175bd5 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -71,7 +71,7 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { onTap: () => showDialog( context: context, builder: (_) => BlocProvider.value( - value: context.watch(), + value: context.read(), child: CurtainModulePrefrencesDialog( deviceId: deviceId, curtainModuleStatusModel: From f43826a82489e88581b2850e87c4fb3ad5053671 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 17:07:59 +0300 Subject: [PATCH 52/75] now it is rendering the chages for motors and control back --- .../curtain_module/view/curtain_module_items.dart | 13 ++++++------- .../curtain_module/widgets/prefrences_dialog.dart | 4 +++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 15175bd5..22ccf90e 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -70,13 +70,12 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { icon: Assets.preferences, onTap: () => showDialog( context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: CurtainModulePrefrencesDialog( - deviceId: deviceId, - curtainModuleStatusModel: - state.curtainModuleStatus, - ), + builder: (_) => CurtainModulePrefrencesDialog( + curtainModuleBloc: + context.watch(), + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, ), ), status: false, diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart index c5fcf4ba..bf500792 100644 --- a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -11,11 +11,12 @@ import 'package:syncrow_web/web_layout/default_container.dart'; class CurtainModulePrefrencesDialog extends StatelessWidget { final CurtainModuleStatusModel curtainModuleStatusModel; final String deviceId; - + final CurtainModuleBloc curtainModuleBloc; const CurtainModulePrefrencesDialog({ super.key, required this.curtainModuleStatusModel, required this.deviceId, + required this.curtainModuleBloc, }); @override @@ -33,6 +34,7 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { ), )), content: BlocBuilder( + bloc: curtainModuleBloc, builder: (context, state) { if (state is CurtainModuleLoading) { return const Center( From 11e285340376b90f0d169b8eb31e66219a8ddb64 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 11:56:43 +0300 Subject: [PATCH 53/75] Enhance AnalyticsDeviceDropdown to show loading indicator during loading state. --- .../widgets/analytics_device_dropdown.dart | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart index f7b33309..055e9675 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart @@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget { ), ), child: Visibility( - visible: state.devices.isNotEmpty, - replacement: _buildNoDevicesFound(context), - child: _buildDevicesDropdown(context, state), + visible: state.status != AnalyticsDevicesStatus.loading, + replacement: _buildLoadingIndicator(), + child: Visibility( + visible: state.devices.isNotEmpty, + replacement: _buildNoDevicesFound(context), + child: _buildDevicesDropdown(context, state), + ), ), ); }, ); } + Widget _buildLoadingIndicator() { + return const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 3), + ), + ); + } + static const _defaultPadding = EdgeInsetsDirectional.symmetric( horizontal: 20, vertical: 2, From 7fda564ee43256336723fec9b0e1fc2bf059a08c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 14:07:24 +0300 Subject: [PATCH 54/75] hotfixes. --- lib/pages/analytics/models/occupancy_heat_map_model.dart | 4 +++- .../modules/air_quality/widgets/aqi_type_dropdown.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 73e7d5d7..cd332745 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -19,7 +19,9 @@ class OccupancyHeatMapModel extends Equatable { eventDate: DateTime.parse( json['event_date'] as String? ?? '${DateTime.now()}', ), - countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0, + countTotalPresenceDetected: num.parse( + json['count_total_presence_detected']?.toString() ?? '900', + ).toInt(), ); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 457bf610..6640c717 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -6,7 +6,7 @@ enum AqiType { aqi('AQI', '', 'aqi'), pm25('PM2.5', 'µg/m³', 'pm25'), pm10('PM10', 'µg/m³', 'pm10'), - hcho('HCHO', 'mg/m³', 'cho2'), + hcho('HCHO', 'mg/m³', 'ch2o'), tvoc('TVOC', 'mg/m³', 'voc'), co2('CO2', 'ppm', 'co2'); From 475462301f8ff72b2b666d8ddadbcf25700cbab9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 15:29:11 +0300 Subject: [PATCH 55/75] manually parse event date for heatmap date object. --- .../analytics/models/occupancy_heat_map_model.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index cd332745..60d1a453 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,13 +14,16 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map json) { + final eventDate = json['event_date'] as String? ?? '${DateTime.now()}'; + final year = eventDate.split('-')[0]; + final month = eventDate.split('-')[1]; + final day = eventDate.split('-')[2]; + return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime.parse( - json['event_date'] as String? ?? '${DateTime.now()}', - ), + eventDate: DateTime(int.parse(year), int.parse(month), int.parse(day)), countTotalPresenceDetected: num.parse( - json['count_total_presence_detected']?.toString() ?? '900', + json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), ); } From 396ce3dad8c55bd1a5757fc3de0f85dd24af0c17 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 16:26:39 +0300 Subject: [PATCH 56/75] now batch is working --- .../device_managment/ac/bloc/ac_bloc.dart | 15 ++- .../helper/route_controls_based_code.dart | 2 +- .../bloc/batch/curtain_module_batch_bloc.dart | 45 ------- .../batch/curtain_module_batch_event.dart | 19 --- .../batch/curtain_module_batch_state.dart | 28 ----- .../bloc/curtain_module_bloc.dart | 118 +++++++++++++++++- .../bloc/curtain_module_event.dart | 63 +++++++++- .../view/curtain_module_batch.dart | 57 +++++---- .../view/curtain_module_items.dart | 22 ++-- .../widgets/curtain_movment_widget.dart | 75 +++++++---- .../shared/device_batch_control_dialog.dart | 5 +- .../batch_control_devices_service.dart | 3 +- 12 files changed, 294 insertions(+), 158 deletions(-) delete mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart delete mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart delete mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index af5a7b0a..9a8e18a2 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -45,7 +45,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -82,10 +83,12 @@ class AcBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); + statusList + .add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = + AcStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } @@ -129,8 +132,10 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index cc09260e..08bca73c 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -213,7 +213,7 @@ mixin RouteControlsBasedCode { case 'CUR_2': return CurtainModuleBatchView( devicesIds: devices - .where((e) => e.productType == 'AC') + .where((e) => e.productType == 'CUR_2') .map((e) => e.uuid!) .toList(), ); diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart deleted file mode 100644 index 87dd53f7..00000000 --- a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:firebase_database/firebase_database.dart'; -import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; -import 'package:syncrow_web/services/devices_mang_api.dart'; - -part 'curtain_module_batch_event.dart'; -part 'curtain_module_batch_state.dart'; - -class CurtainModuleBatchBloc - extends Bloc { - final ControlDeviceService controlDeviceService; - StreamSubscription? _firebaseSubscription; - - CurtainModuleBatchBloc(this.controlDeviceService) - : super(CurtainModuleBatchInitial()) { - on(_onFetchAcBatchStatus); - } - - Future _onFetchAcBatchStatus( - CutrainModuleFetchBatchStatusEvent event, - Emitter emit, - ) async { - emit(CurtainModuleBatchLoadingState()); - try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - status.status.forEach( - (element) => print( - 'this is code ${element.code} - this is value ${element.value}'), - ); - - emit( - CurtainModuleBatchLoadedState( - curtainModuleStatusModel: CurtainModuleStatusModel.fromJson({}), - ), - ); - } catch (e) { - emit(CurtainModuleBatchFailedState(error: e.toString())); - } - } -} diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart deleted file mode 100644 index 351773f9..00000000 --- a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of 'curtain_module_batch_bloc.dart'; - -sealed class CurtainModuleBatchEvent extends Equatable { - const CurtainModuleBatchEvent(); - - @override - List get props => []; -} - -class CutrainModuleFetchBatchStatusEvent extends CurtainModuleBatchEvent { - final List devicesIds; - - const CutrainModuleFetchBatchStatusEvent({ - required this.devicesIds, - }); - - @override - List get props => [devicesIds]; -} diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart deleted file mode 100644 index bb8bb7d0..00000000 --- a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of 'curtain_module_batch_bloc.dart'; - -sealed class CurtainModuleBatchState extends Equatable { - const CurtainModuleBatchState(); - - @override - List get props => []; -} - -final class CurtainModuleBatchInitial extends CurtainModuleBatchState {} - -final class CurtainModuleBatchLoadingState extends CurtainModuleBatchState {} - -final class CurtainModuleBatchLoadedState extends CurtainModuleBatchState { - final CurtainModuleStatusModel curtainModuleStatusModel; - const CurtainModuleBatchLoadedState({ - required this.curtainModuleStatusModel, - }); -} - -final class CurtainModuleBatchFailedState extends CurtainModuleBatchState { - final String error; - - const CurtainModuleBatchFailedState({required this.error}); - - @override - List get props => [error]; -} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart index d79380cf..c3cd0b92 100644 --- a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -3,7 +3,9 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -12,9 +14,13 @@ part 'curtain_module_state.dart'; class CurtainModuleBloc extends Bloc { final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; StreamSubscription? _firebaseSubscription; - CurtainModuleBloc(this.controlDeviceService) : super(CurtainModuleInitial()) { + CurtainModuleBloc({ + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CurtainModuleInitial()) { on(_onFetchCurtainModuleStatusEvent); on(_onSendCurtainPercentToApiEvent); on(_onOpenCurtainEvent); @@ -26,6 +32,13 @@ class CurtainModuleBloc extends Bloc { on(_onChangeControlBackEvent); on(_onChangeControlBackModeEvent); on(_onChangeCurtainModuleStatusEvent); + //batch + on(_onFetchCurtainModuleBatchStatus); + on(_onSendCurtainBatchPercentToApiEvent); + on(_onOpenCurtainBatchEvent); + on(_onCloseCurtainBatchEvent); + on(_onStopCurtainBatchEvent); + on(_onFactoryReset); } Future _onFetchCurtainModuleStatusEvent( @@ -224,6 +237,109 @@ class CurtainModuleBloc extends Bloc { } } + FutureOr _onFetchCurtainModuleBatchStatus( + CurtainModuleFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + + final result = Map.fromEntries( + status.status.map((element) => MapEntry(element.code, element.value)), + ); + + emit(CurtainModuleStatusLoaded( + curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), + )); + } catch (e) { + emit(CurtainModuleError(message: e.toString())); + } + } + + Future _onSendCurtainBatchPercentToApiEvent( + SendCurtainBatchPercentToApiEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: event.status.code, + value: event.status.value, + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to send control command: $e')); + } + } + + Future _onOpenCurtainBatchEvent( + OpenCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'open', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to open curtain: $e')); + } + } + + Future _onCloseCurtainBatchEvent( + CloseCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'close', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to close curtain: $e')); + } + } + + Future _onStopCurtainBatchEvent( + StopCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'stop', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to stop curtain: $e')); + } + } + + Future _onFactoryReset( + CurtainModuleFactoryReset event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const CurtainModuleError(message: 'Failed')); + } else { + add( + FetchCurtainModuleStatusEvent(deviceId: event.deviceId), + ); + } + } catch (e) { + emit(CurtainModuleError(message: e.toString())); + } + } + @override Future close() async { await _firebaseSubscription?.cancel(); diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart index 6b0d89ae..4eec030d 100644 --- a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart @@ -129,4 +129,65 @@ class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent { @override List get props => [deviceId, status]; -} \ No newline at end of file +} + +///batch +class CurtainModuleFetchBatchStatusEvent extends CurtainModuleEvent { + final List devicesIds; + + const CurtainModuleFetchBatchStatusEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class SendCurtainBatchPercentToApiEvent extends CurtainModuleEvent { + final List devicesId; + final Status status; + + const SendCurtainBatchPercentToApiEvent({ + required this.devicesId, + required this.status, + }); + + @override + List get props => [devicesId, status]; +} + +class OpenCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const OpenCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class CloseCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const CloseCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class StopCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const StopCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class CurtainModuleFactoryReset extends CurtainModuleEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const CurtainModuleFactoryReset( + {required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart index 0e0dfdc3..bd28cd8a 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -17,8 +20,10 @@ class CurtainModuleBatchView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainModuleBatchBloc(RemoteControlDeviceService()) - ..add(CutrainModuleFetchBatchStatusEvent(devicesIds: devicesIds)), + create: (context) => CurtainModuleBloc( + controlDeviceService: RemoteControlDeviceService(), + batchControlDevicesService: RemoteBatchControlDevicesService()) + ..add(CurtainModuleFetchBatchStatusEvent(devicesIds)), child: _buildStatusControls(context), ); } @@ -30,37 +35,41 @@ class CurtainModuleBatchView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ControlCurtainMovementWidget( - deviceId: devicesIds.first, + devicesId: devicesIds, ), const SizedBox( height: 10, ), SizedBox( height: 120, - width: 350, + // width: 350, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: IconNameStatusContainer( - isFullIcon: false, - name: 'Factory Reset', - icon: Assets.factoryReset, - onTap: () {}, - status: false, - textColor: ColorsManager.blackColor, - ), - ), - Expanded( - child: IconNameStatusContainer( - isFullIcon: false, - name: 'Firmware Update', - icon: Assets.firmware, - onTap: () {}, - status: false, - textColor: ColorsManager.blackColor, - ), + // Expanded( + // child: + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CurtainModuleFactoryReset( + deviceId: devicesIds.first, + factoryReset: + FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, ), + // ), + // Expanded( + // child: IconNameStatusContainer( + // isFullIcon: false, + // name: 'Firmware Update', + // icon: Assets.firmware, + // onTap: () {}, + // status: false, + // textColor: ColorsManager.blackColor, + // ), + // ) ], ), ), diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 22ccf90e..198c8713 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -19,7 +20,9 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainModuleBloc(RemoteControlDeviceService()) + create: (context) => CurtainModuleBloc( + controlDeviceService: RemoteControlDeviceService(), + batchControlDevicesService: RemoteBatchControlDevicesService()) ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), child: _buildStatusControls(context), ); @@ -32,7 +35,7 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { crossAxisAlignment: CrossAxisAlignment.start, children: [ ControlCurtainMovementWidget( - deviceId: deviceId, + devicesId: [deviceId], ), const SizedBox( height: 10, @@ -70,12 +73,15 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { icon: Assets.preferences, onTap: () => showDialog( context: context, - builder: (_) => CurtainModulePrefrencesDialog( - curtainModuleBloc: - context.watch(), - deviceId: deviceId, - curtainModuleStatusModel: - state.curtainModuleStatus, + builder: (_) => BlocProvider.value( + value: context.read(), + child: CurtainModulePrefrencesDialog( + curtainModuleBloc: + context.watch(), + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, + ), ), ), status: false, diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart index 076c5a9c..e98ff11d 100644 --- a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart @@ -9,10 +9,10 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class ControlCurtainMovementWidget extends StatelessWidget { - final String deviceId; + final List devicesId; const ControlCurtainMovementWidget({ super.key, - required this.deviceId, + required this.devicesId, }); @override @@ -26,9 +26,15 @@ class ControlCurtainMovementWidget extends StatelessWidget { CurtainActionWidget( icon: Assets.openCurtain, onTap: () { - context.read().add( - OpenCurtainEvent(deviceId: deviceId), - ); + if (devicesId.length == 1) { + context.read().add( + OpenCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + OpenCurtainBatchEvent(devicesId: devicesId), + ); + } }, ), const SizedBox( @@ -37,9 +43,15 @@ class ControlCurtainMovementWidget extends StatelessWidget { CurtainActionWidget( icon: Assets.pauseCurtain, onTap: () { - context.read().add( - StopCurtainEvent(deviceId: deviceId), - ); + if (devicesId.length == 1) { + context.read().add( + StopCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + StopCurtainBatchEvent(devicesId: devicesId), + ); + } }, ), const SizedBox( @@ -48,9 +60,15 @@ class ControlCurtainMovementWidget extends StatelessWidget { CurtainActionWidget( icon: Assets.closeCurtain, onTap: () { - context.read().add( - CloseCurtainEvent(deviceId: deviceId), - ); + if (devicesId.length == 1) { + context.read().add( + CloseCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + CloseCurtainBatchEvent(devicesId: devicesId), + ); + } }, ), BlocBuilder( @@ -84,7 +102,7 @@ class ControlCurtainMovementWidget extends StatelessWidget { } else if (state is CurtainModuleStatusLoaded) { return CurtainSliderWidget( status: state.curtainModuleStatus, - deviceId: deviceId, + devicesId: devicesId, ); } else { return const Center( @@ -108,12 +126,12 @@ class ControlCurtainMovementWidget extends StatelessWidget { class CurtainSliderWidget extends StatefulWidget { final CurtainModuleStatusModel status; - final String deviceId; + final List devicesId; const CurtainSliderWidget({ super.key, required this.status, - required this.deviceId, + required this.devicesId, }); @override @@ -167,16 +185,27 @@ class _CurtainSliderWidgetState extends State { onChangeEnd: (value) { final int targetPercent = (value * 100).round(); - // Dispatch API call - context.read().add( - SendCurtainPercentToApiEvent( - deviceId: widget.deviceId, - status: Status( - code: 'percent_control', - value: targetPercent, + if (widget.devicesId.length == 1) { + context.read().add( + SendCurtainPercentToApiEvent( + deviceId: widget.devicesId.first, + status: Status( + code: 'percent_control', + value: targetPercent, + ), ), - ), - ); + ); + } else { + context.read().add( + SendCurtainBatchPercentToApiEvent( + devicesId: widget.devicesId, + status: Status( + code: 'percent_control', + value: targetPercent, + ), + ), + ); + } // Revert back to Firebase-synced stream setState(() { diff --git a/lib/pages/device_managment/shared/device_batch_control_dialog.dart b/lib/pages/device_managment/shared/device_batch_control_dialog.dart index f2dc68f5..c7ea6c71 100644 --- a/lib/pages/device_managment/shared/device_batch_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_batch_control_dialog.dart @@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCode { +class DeviceBatchControlDialog extends StatelessWidget + with RouteControlsBasedCode { final List devices; const DeviceBatchControlDialog({super.key, required this.devices}); @@ -18,7 +19,7 @@ class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCo borderRadius: BorderRadius.circular(20), ), child: SizedBox( - width: devices.length < 2 ? 500 : 800, + width: devices.length < 2 ? 600 : 800, // height: context.screenHeight * 0.7, child: SingleChildScrollView( child: Padding( diff --git a/lib/services/batch_control_devices_service.dart b/lib/services/batch_control_devices_service.dart index f78cdef4..16542c8c 100644 --- a/lib/services/batch_control_devices_service.dart +++ b/lib/services/batch_control_devices_service.dart @@ -11,7 +11,8 @@ abstract interface class BatchControlDevicesService { }); } -final class RemoteBatchControlDevicesService implements BatchControlDevicesService { +final class RemoteBatchControlDevicesService + implements BatchControlDevicesService { @override Future batchControlDevices({ required List uuids, From ca41aa622479d28c10478620546e8447bfcbac9f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 16:37:09 +0300 Subject: [PATCH 57/75] all dates in heatmap are utc. --- .../analytics/models/occupancy_heat_map_model.dart | 14 +++++++++----- .../occupancy/widgets/interactive_heat_map.dart | 2 +- .../occupancy/widgets/occupancy_heat_map.dart | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 60d1a453..e4b7d730 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,14 +14,18 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map json) { - final eventDate = json['event_date'] as String? ?? '${DateTime.now()}'; - final year = eventDate.split('-')[0]; - final month = eventDate.split('-')[1]; - final day = eventDate.split('-')[2]; + final eventDate = json['event_date'] as String?; + final year = eventDate?.split('-')[0]; + final month = eventDate?.split('-')[1]; + final day = eventDate?.split('-')[2]; return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime(int.parse(year), int.parse(month), int.parse(day)), + eventDate: DateTime( + int.parse(year ?? '2025'), + int.parse(month ?? '1'), + int.parse(day ?? '1'), + ).toUtc(), countTotalPresenceDetected: num.parse( json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), diff --git a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart index a652ae73..514ebb65 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart @@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State { color: Colors.transparent, child: Transform.translate( offset: Offset(-(widget.cellSize * 2.5), -50), - child: HeatMapTooltip(date: item.date, value: item.value), + child: HeatMapTooltip(date: item.date.toUtc(), value: item.value), ), ), ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 05415421..0809a990 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc(); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } List _generatePaintItems(DateTime startDate) { return List.generate(_totalWeeks * 7, (index) { - final date = startDate.add(Duration(days: index)); + final date = startDate.toUtc().add(Duration(days: index)); final value = heatMapData[date] ?? 0; return OccupancyPaintItem(index: index, value: value, date: date); }); From 812c51400b984a5fe553f0bb57fe987a9e4b419c Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 16:53:40 +0300 Subject: [PATCH 58/75] add listener to batch --- .../bloc/curtain_module_bloc.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart index c3cd0b92..b40d7ea6 100644 --- a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -253,6 +253,37 @@ class CurtainModuleBloc extends Bloc { emit(CurtainModuleStatusLoaded( curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), )); + + Map statusMap = {}; + final ref = FirebaseDatabase.instance + .ref('device-status/${event.devicesIds.first}'); + final stream = ref.onValue; + + stream.listen((DatabaseEvent DatabaseEvent) async { + if (DatabaseEvent.snapshot.value == null) return; + + Map usersMap = + DatabaseEvent.snapshot.value as Map; + + List statusList = []; + + usersMap['status'].forEach((element) { + statusList + .add(Status(code: element['code'], value: element['value'])); + }); + + statusMap = { + for (final element in statusList) element.code: element.value, + }; + if (!isClosed) { + add( + ChangeCurtainModuleStatusEvent( + deviceId: event.devicesIds.first, + status: CurtainModuleStatusModel.fromJson(statusMap), + ), + ); + } + }); } catch (e) { emit(CurtainModuleError(message: e.toString())); } From 4744009cb65c887a80346a898733c032813877d7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 17:11:17 +0300 Subject: [PATCH 59/75] Update TotalEnergyConsumptionChart to adjust Y-axis limits and intervals for better data representation --- .../widgets/total_energy_consumption_chart.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 85b95c29..bba1fa14 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget { return Expanded( child: LineChart( LineChartData( + maxY: chartData.isEmpty + ? null + : chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250, clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, - leftTitlesInterval: 250, + leftTitlesInterval: 500, ), gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, - horizontalInterval: 250, + horizontalInterval: 500, ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(), @@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { ), duration: Duration.zero, curve: Curves.easeIn, - ), ); } From 0c0bf96c073dfd090cfb93c8a1a48c477cd3dbf8 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:16:41 +0300 Subject: [PATCH 60/75] add bloc builder to use the context --- .../view/curtain_module_items.dart | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 198c8713..07dc4158 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; @@ -24,7 +26,11 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { controlDeviceService: RemoteControlDeviceService(), batchControlDevicesService: RemoteBatchControlDevicesService()) ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), - child: _buildStatusControls(context), + child: BlocBuilder( + builder: (context, state) { + return _buildStatusControls(context); + }, + ), ); } @@ -41,19 +47,28 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { height: 10, ), SizedBox( - height: 120, + height: 140, width: 350, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: IconNameStatusContainer( - isFullIcon: false, - name: 'Schedules', - icon: Assets.schedule, - onTap: () {}, - status: false, - textColor: ColorsManager.blackColor, + child: ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'CUR_2', + ), + )); + }, + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ), const SizedBox( From 26e8ff7ee2adfe95fbcb70105245b4011ad4e217 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:55:27 +0300 Subject: [PATCH 61/75] use dynamic instead of bool to accept mant types and fix schedual view to accept curtain code and value --- .../schedule_widgets/schedual_view.dart | 16 +++++++++++++--- .../helper/add_schedule_dialog_helper.dart | 6 +++++- lib/services/devices_mang_api.dart | 3 +-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 47534d37..3b7bc138 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -12,10 +12,17 @@ import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_sched import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { - const BuildScheduleView( - {super.key, required this.deviceUuid, required this.category}); + const BuildScheduleView({ + super.key, + required this.deviceUuid, + required this.category, + this.code, + this.value, + }); final String deviceUuid; final String category; + final String? code; + final String? value; @override Widget build(BuildContext context) { @@ -59,11 +66,14 @@ class BuildScheduleView extends StatelessWidget { context, schedule: null, isEdit: false, + code: code, + value: value, ); if (entry != null) { context.read().add( ScheduleAddEvent( category: entry.category, + code: entry.function.code, time: entry.time, functionOn: entry.function.value, selectedDays: entry.days, @@ -74,7 +84,7 @@ class BuildScheduleView extends StatelessWidget { ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - CountdownInchingView( + CountdownInchingView( deviceId: deviceUuid, ), const SizedBox(height: 20), diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index ae7feac9..b8059402 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -17,6 +17,8 @@ class ScheduleDialogHelper { BuildContext context, { ScheduleEntry? schedule, bool isEdit = false, + String? code, + String? value, }) { final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) @@ -118,7 +120,9 @@ class ScheduleDialogHelper { final entry = ScheduleEntry( category: schedule?.category ?? 'switch_1', time: _formatTimeOfDayToISO(selectedTime), - function: Status(code: 'switch_1', value: functionOn), + function: Status( + code: code ?? 'switch_1', + value: value ?? functionOn), days: _convertSelectedDaysToStrings(selectedDays), scheduleId: schedule?.scheduleId, ); diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6fb27daf..963738a7 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -393,7 +393,7 @@ class DevicesManagementApi { required String deviceId, required String time, required String code, - required bool value, + required dynamic value, required List days, }) async { final response = await HTTPService().post( @@ -416,5 +416,4 @@ class DevicesManagementApi { ); return response; } - } From e365aa3faac702c29430f5cde7755f3bebdadc0b Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:56:12 +0300 Subject: [PATCH 62/75] edite event and block of schdual to accept code and functionOn as dynamic --- .../schedule_device/bloc/schedule_bloc.dart | 2 +- .../schedule_device/bloc/schedule_event.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index fbf7ae64..0ec55e39 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -265,7 +265,7 @@ class ScheduleBloc extends Bloc { category: event.category, deviceId: deviceId, time: getTimeStampWithoutSeconds(dateTime).toString(), - code: event.category, + code: event.code ?? event.category, value: event.functionOn, days: event.selectedDays); if (success) { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 0b9ec581..a28b8757 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -70,17 +70,19 @@ class ScheduleAddEvent extends ScheduleEvent { final String category; final String time; final List selectedDays; - final bool functionOn; + final dynamic functionOn; + final String? code; const ScheduleAddEvent({ required this.category, required this.time, required this.selectedDays, required this.functionOn, + required this.code, }); @override - List get props => [category, time, selectedDays, functionOn]; + List get props => [category, time, selectedDays, functionOn, code]; } class ScheduleEditEvent extends ScheduleEvent { From 1d95915f57be5788db5b458d2aa989ede7d67ac8 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:56:41 +0300 Subject: [PATCH 63/75] fix the string for motor without underscore --- .../curtain_module/widgets/prefrences_dialog.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart index bf500792..1e4f932c 100644 --- a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -69,7 +69,8 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { }, ), PrefReversCardWidget( - title: state.curtainModuleStatus.elecMachineryMode, + title: formatDeviceType( + state.curtainModuleStatus.elecMachineryMode), body: 'Motor Mode', onTap: () => context.read().add( ChangeElecMachineryModeEvent( @@ -136,4 +137,13 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { ), ); } + + String formatDeviceType(String raw) { + return raw + .split('_') + .map((word) => word.isNotEmpty + ? '${word[0].toUpperCase()}${word.substring(1)}' + : '') + .join(' '); + } } From 32208c1e81976a09d1311a072b7bc0dc39a5a94f Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:57:04 +0300 Subject: [PATCH 64/75] send code and value to schdual from curtain module --- .../curtain_module/view/curtain_module_items.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 07dc4158..48b5ab2a 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -63,6 +63,8 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { child: BuildScheduleView( deviceUuid: deviceId, category: 'CUR_2', + code: 'control', + value: 'open', ), )); }, From 13e9a808ab70e0fe745d25ac9eadfb72cfcf5845 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 22:09:00 +0300 Subject: [PATCH 65/75] uses UTC dates as an attempt to fix heatmap's rendering bug. --- lib/pages/analytics/models/occupancy_heat_map_model.dart | 4 ++-- .../modules/occupancy/widgets/occupancy_heat_map.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index e4b7d730..4c7a37d4 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -21,11 +21,11 @@ class OccupancyHeatMapModel extends Equatable { return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime( + eventDate: DateTime.utc( int.parse(year ?? '2025'), int.parse(month ?? '1'), int.parse(day ?? '1'), - ).toUtc(), + ), countTotalPresenceDetected: num.parse( json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 0809a990..05c5bb71 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc(); + final jan1 = DateTime.utc(DateTime.now().year, 1, 1); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } List _generatePaintItems(DateTime startDate) { return List.generate(_totalWeeks * 7, (index) { - final date = startDate.toUtc().add(Duration(days: index)); + final date = startDate.add(Duration(days: index)); final value = heatMapData[date] ?? 0; return OccupancyPaintItem(index: index, value: value, date: date); }); From c4fd90b3bca1aa3ca390b1330c606c3e3c32fee1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sat, 28 Jun 2025 13:16:06 +0300 Subject: [PATCH 66/75] testing heatmap fixes. --- .../modules/occupancy/widgets/occupancy_heat_map.dart | 9 +++++++-- .../occupancy/widgets/occupancy_heat_map_box.dart | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 05415421..482f0029 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_ import 'package:syncrow_web/utils/color_manager.dart'; class OccupancyHeatMap extends StatelessWidget { - const OccupancyHeatMap({required this.heatMapData, super.key}); + const OccupancyHeatMap({ + required this.heatMapData, + required this.selectedDate, + super.key, + }); final Map heatMapData; + final DateTime selectedDate; static const _cellSize = 16.0; static const _totalWeeks = 53; @@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime.utc(selectedDate.year, 1, 1); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart index c3b537e0..a5f56aa4 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -70,6 +70,8 @@ class OccupancyHeatMapBox extends StatelessWidget { const SizedBox(height: 20), Expanded( child: OccupancyHeatMap( + selectedDate: + context.watch().state.yearlyDate, heatMapData: state.heatMapData.asMap().map( (_, value) => MapEntry( value.eventDate, From b97183fb61baef13f4018c6202ede44814f0f8ce Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sat, 28 Jun 2025 15:47:42 +0400 Subject: [PATCH 67/75] SP-1801 --- .../device_managment/ac/bloc/ac_bloc.dart | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 38d11a46..2ac26b41 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -45,8 +45,7 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -74,22 +73,18 @@ class AcBloc extends Bloc { void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - _deviceStatusSubscription = - ref.onValue.listen((DatabaseEvent event) async { + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; - Map usersMap = - event.snapshot.value as Map; + final usersMap = event.snapshot.value! as Map; - List statusList = []; + final statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = - AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); print('Device status updated: ${deviceStatus.acSwitch}'); if (!isClosed) { @@ -111,21 +106,14 @@ class AcBloc extends Bloc { AcControlEvent event, Emitter emit, ) async { - emit(AcsLoadingState()); - try { - final success = await controlDeviceService.controlDevice( + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); + await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); - if (!success) { - emit(const AcsFailedState(error: 'Failed to control device')); - } - } catch (e) { - emit(AcsFailedState(error: e.toString())); - } + } catch (e) {} } FutureOr _onFetchAcBatchStatus( @@ -134,10 +122,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -148,23 +134,16 @@ class AcBloc extends Bloc { AcBatchControlEvent event, Emitter emit, ) async { - emit(AcsLoadingState()); _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); try { - final success = await batchControlDevicesService.batchControlDevices( + await batchControlDevicesService.batchControlDevices( uuids: event.devicesIds, code: event.code, value: event.value, ); - - if (!success) { - emit(const AcsFailedState(error: 'Failed to control devices')); - } - } catch (e) { - emit(AcsFailedState(error: e.toString())); - } + } catch (e) {} } Future _onFactoryReset( @@ -197,8 +176,8 @@ class AcBloc extends Bloc { void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; - int newHours = scheduledHours; - int newMinutes = scheduledMinutes + 30; + var newHours = scheduledHours; + var newMinutes = scheduledMinutes + 30; newHours += newMinutes ~/ 60; newMinutes = newMinutes % 60; if (newHours > 23) { @@ -220,7 +199,7 @@ class AcBloc extends Bloc { ) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; - int totalMinutes = (scheduledHours * 60) + scheduledMinutes; + var totalMinutes = (scheduledHours * 60) + scheduledMinutes; totalMinutes = (totalMinutes - 30).clamp(0, 1440); scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; @@ -293,7 +272,7 @@ class AcBloc extends Bloc { void _startCountdownTimer(Emitter emit) { _countdownTimer?.cancel(); - int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); + var totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (totalSeconds > 0) { From bd53388438f04f3a15b87562f0770d169cf91a81 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 09:17:22 +0300 Subject: [PATCH 68/75] make one API with new QP to filter on spacesId --- .../device_managment_bloc.dart | 14 ++-- .../bloc/routine_bloc/routine_bloc.dart | 80 +++++++++---------- lib/services/devices_mang_api.dart | 13 ++- lib/utils/constants/api_const.dart | 2 +- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 98b0c195..6fd23374 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -41,18 +41,17 @@ class DeviceManagementBloc _devices.clear(); var spaceBloc = event.context.read(); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (spaceBloc.state.selectedCommunities.isEmpty) { - devices = - await DevicesManagementApi().fetchDevices('', '', projectUuid); + devices = await DevicesManagementApi().fetchDevices(projectUuid); } else { for (var community in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; - for (var space in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(community, space, projectUuid)); - } + spacesList.forEach( + (element) => print(element), + ); + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } @@ -270,6 +269,7 @@ class DeviceManagementBloc return 'All'; } } + void _onSearchDevices( SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 3fd07834..f38ea994 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -170,45 +170,45 @@ class RoutineBloc extends Bloc { } } -Future _onLoadScenes( - LoadScenes event, Emitter emit) async { - emit(state.copyWith(isLoading: true, errorMessage: null)); - List scenes = []; - try { - BuildContext context = NavigationService.navigatorKey.currentContext!; - var createRoutineBloc = context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (createRoutineBloc.selectedSpaceId == '' && - createRoutineBloc.selectedCommunityId == '') { - var spaceBloc = context.read(); - for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = - spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - scenes.addAll( - await SceneApi.getScenes(spaceId, communityId, projectUuid)); + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + List scenes = []; + try { + BuildContext context = NavigationService.navigatorKey.currentContext!; + var createRoutineBloc = context.read(); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { + var spaceBloc = context.read(); + for (var communityId in spaceBloc.state.selectedCommunities) { + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + for (var spaceId in spacesList) { + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); + } } + } else { + scenes.addAll(await SceneApi.getScenes( + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } - } else { - scenes.addAll(await SceneApi.getScenes( - createRoutineBloc.selectedSpaceId, - createRoutineBloc.selectedCommunityId, - projectUuid)); - } - emit(state.copyWith( - scenes: scenes, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( + emit(state.copyWith( + scenes: scenes, isLoading: false, - loadScenesErrorMessage: 'Failed to load scenes', - errorMessage: '', - loadAutomationErrorMessage: '', - scenes: scenes)); + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', + scenes: scenes)); + } } -} Future _onLoadAutomation( LoadAutomation event, Emitter emit) async { @@ -936,16 +936,12 @@ Future _onLoadScenes( for (var communityId in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(communityId, spaceId, projectUuid)); - } + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } else { - devices.addAll(await DevicesManagementApi().fetchDevices( - createRoutineBloc.selectedCommunityId, - createRoutineBloc.selectedSpaceId, - projectUuid)); + devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid, + spacesId: [createRoutineBloc.selectedSpaceId])); } emit(state.copyWith(isLoading: false, devices: devices)); diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6fb27daf..a6b4a278 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -13,15 +13,13 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class DevicesManagementApi { Future> fetchDevices( - String communityId, String spaceId, String projectId) async { + String projectId, { + List? spacesId, + }) async { try { final response = await HTTPService().get( - path: communityId.isNotEmpty && spaceId.isNotEmpty - ? ApiEndpoints.getSpaceDevices - .replaceAll('{spaceUuid}', spaceId) - .replaceAll('{communityUuid}', communityId) - .replaceAll('{projectId}', projectId) - : ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), + queryParameters: {if (spacesId != null) 'spaces': spacesId}, + path: ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), showServerMessage: true, expectedResponseModel: (json) { List jsonData = json['data']; @@ -416,5 +414,4 @@ class DevicesManagementApi { ); return response; } - } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index eb7b6a3e..6dda1108 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -18,7 +18,7 @@ abstract class ApiEndpoints { static const String getAllDevices = '/projects/{projectId}/devices'; static const String getSpaceDevices = - '/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices'; + '/projects/{projectId}/devices'; static const String getDeviceStatus = '/devices/{uuid}/functions/status'; static const String getBatchStatus = '/devices/batch'; From 1828ffb87a2903c8d2340cb4758fef81e0bb23a8 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 09:17:57 +0300 Subject: [PATCH 69/75] remove print statment --- .../bloc/device_mgmt_bloc/device_managment_bloc.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 6fd23374..4063692e 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -47,9 +47,6 @@ class DeviceManagementBloc for (var community in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; - spacesList.forEach( - (element) => print(element), - ); devices.addAll(await DevicesManagementApi() .fetchDevices(projectUuid, spacesId: spacesList)); } From b96f65d2c2b0cc577996e94101eb5a57212063c4 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 10:42:18 +0300 Subject: [PATCH 70/75] fix the open close states when curtain module --- .../curtain_module/view/curtain_module_items.dart | 2 +- .../schedule_widgets/schedual_view.dart | 15 ++++++++++----- .../schedule_widgets/schedule_table.dart | 14 ++++++++++++-- .../helper/add_schedule_dialog_helper.dart | 14 +++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 48b5ab2a..82c812ce 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -64,7 +64,7 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { deviceUuid: deviceId, category: 'CUR_2', code: 'control', - value: 'open', + ), )); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 3b7bc138..c511b8bd 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart'; @@ -9,6 +10,7 @@ import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widg import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { @@ -17,12 +19,10 @@ class BuildScheduleView extends StatelessWidget { required this.deviceUuid, required this.category, this.code, - this.value, }); final String deviceUuid; final String category; final String? code; - final String? value; @override Widget build(BuildContext context) { @@ -64,15 +64,20 @@ class BuildScheduleView extends StatelessWidget { final entry = await ScheduleDialogHelper .showAddScheduleDialog( context, - schedule: null, + schedule: ScheduleEntry( + category: category, + time: '', + function: Status( + code: code.toString(), value: null), + days: [], + ), isEdit: false, code: code, - value: value, ); if (entry != null) { context.read().add( ScheduleAddEvent( - category: entry.category, + category: category, code: entry.function.code, time: entry.time, functionOn: entry.function.value, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 98ae0515..b23e48df 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -162,11 +162,18 @@ class _ScheduleTableView extends StatelessWidget { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + bool temp; + if (schedule.category == 'CUR_2') { + temp = schedule.function.value == 'open' ? true : false; + } else { + temp = schedule.function.value as bool; + } context.read().add( ScheduleUpdateEntryEvent( category: schedule.category, scheduleId: schedule.scheduleId, - functionOn: schedule.function.value, + functionOn: temp, + // schedule.function.value, enable: !schedule.enable, ), ); @@ -188,7 +195,10 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - Center(child: Text(schedule.function.value ? 'On' : 'Off')), + schedule.category == 'CUR_2' + ? Center( + child: Text(schedule.function.value == true ? 'open' : 'close')) + : Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index b8059402..51087704 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -18,7 +18,6 @@ class ScheduleDialogHelper { ScheduleEntry? schedule, bool isEdit = false, String? code, - String? value, }) { final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) @@ -117,12 +116,21 @@ class ScheduleDialogHelper { width: 100, child: ElevatedButton( onPressed: () { + dynamic temp; + if (schedule?.category == 'CUR_2') { + temp = functionOn! ? 'open' : 'close'; + } else { + temp = functionOn; + } + print(temp); final entry = ScheduleEntry( category: schedule?.category ?? 'switch_1', time: _formatTimeOfDayToISO(selectedTime), function: Status( - code: code ?? 'switch_1', - value: value ?? functionOn), + code: code ?? 'switch_1', + value: temp, + // functionOn, + ), days: _convertSelectedDaysToStrings(selectedDays), scheduleId: schedule?.scheduleId, ); From ec1bb5b609bc54f6fab908b212aa8c297ffedda4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 29 Jun 2025 10:49:00 +0300 Subject: [PATCH 71/75] added curtain icons. --- lib/pages/visitor_password/model/device_model.dart | 4 ++++ lib/utils/enum/device_types.dart | 2 ++ 2 files changed, 6 insertions(+) diff --git a/lib/pages/visitor_password/model/device_model.dart b/lib/pages/visitor_password/model/device_model.dart index f9711eed..75d00350 100644 --- a/lib/pages/visitor_password/model/device_model.dart +++ b/lib/pages/visitor_password/model/device_model.dart @@ -80,6 +80,10 @@ class DeviceModel { tempIcon = Assets.openedDoor; } else if (type == DeviceType.WaterLeak) { tempIcon = Assets.waterLeakNormal; + } else if (type == DeviceType.Curtain2) { + tempIcon = Assets.curtainIcon; + } else if (type == DeviceType.Curtain) { + tempIcon = Assets.curtainIcon; } else { tempIcon = Assets.blackLogo; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 9bfd322f..947e63aa 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -3,6 +3,7 @@ enum DeviceType { LightBulb, DoorLock, Curtain, + Curtain2, Blind, OneGang, TwoGang, @@ -44,6 +45,7 @@ enum DeviceType { Map devicesTypesMap = { "AC": DeviceType.AC, + "CUR_2": DeviceType.Curtain2, "GW": DeviceType.Gateway, "CPS": DeviceType.CeilingSensor, "DL": DeviceType.DoorLock, From a1562110d57e47f5d583c507b0f37b3abf3b6d97 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 10:50:51 +0300 Subject: [PATCH 72/75] add close open if it is curtain module for schdule --- .../water_heater/helper/add_schedule_dialog_helper.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 51087704..389eac3f 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -97,7 +97,8 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(ctx, functionOn!, (v) { + _buildFunctionSwitch(schedule!.category, ctx, functionOn!, + (v) { setState(() => functionOn = v); }), ], @@ -197,7 +198,7 @@ class ScheduleDialogHelper { } static Widget _buildFunctionSwitch( - BuildContext ctx, bool isOn, Function(bool) onChanged) { + String categor, BuildContext ctx, bool isOn, Function(bool) onChanged) { return Row( children: [ Text( @@ -211,14 +212,14 @@ class ScheduleDialogHelper { groupValue: isOn, onChanged: (val) => onChanged(true), ), - const Text('On'), + Text(categor == 'CUR_2' ? 'open' : 'On'), const SizedBox(width: 10), Radio( value: false, groupValue: isOn, onChanged: (val) => onChanged(false), ), - const Text('Off'), + Text(categor == 'CUR_2' ? 'close' : 'Off'), ], ); } From 23cfee14901c7b176b4a20ba0a76fbc65c35d702 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 11:12:28 +0300 Subject: [PATCH 73/75] fix curtain name in curtain if then containers dialogs --- .../routines/widgets/routine_dialogs/curtain_dialog.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart index bdf8660d..64295e2a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -58,7 +58,9 @@ class CurtainHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('AC Functions'), + DialogHeader(dialogType == 'THEN' + ? 'Curtain Functions' + : 'Curtain Conditions'), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, From 388391eec4b900326aac70776f22b175c0bd19cd Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 11:29:43 +0300 Subject: [PATCH 74/75] stop stacking snackbars --- .../all_devices/widgets/device_managment_body.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index c865a5dc..4f86c8e3 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -111,6 +111,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { onPressed: isControlButtonEnabled ? () { if (isAnyDeviceOffline) { + ScaffoldMessenger.of(context) + .clearSnackBars(); ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( From 19cdd371f8bb212696585e6467bb2d28ed8839be Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 12:43:23 +0300 Subject: [PATCH 75/75] fix edit problem and funtion name in dialog was olways keep close now it is really take the real value --- .../schedule_device/schedule_widgets/schedule_table.dart | 8 ++++---- .../water_heater/helper/add_schedule_dialog_helper.dart | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index b23e48df..21f404ff 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -195,10 +195,10 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - schedule.category == 'CUR_2' - ? Center( - child: Text(schedule.function.value == true ? 'open' : 'close')) - : Center(child: Text(schedule.function.value ? 'On' : 'Off')), + if (schedule.category == 'CUR_2') + Center(child: Text(schedule.function.value)) + else + Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 389eac3f..f55b32ab 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -19,13 +19,19 @@ class ScheduleDialogHelper { bool isEdit = false, String? code, }) { + bool temp; + if (schedule?.category == 'CUR_2') { + temp = schedule!.function.value == 'open' ? true : false; + } else { + temp = schedule!.function.value; + } final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) : TimeOfDay.now(); final initialDays = schedule != null ? _convertDaysStringToBooleans(schedule.days) : List.filled(7, false); - bool? functionOn = schedule?.function.value ?? true; + bool? functionOn = temp; TimeOfDay selectedTime = initialTime; List selectedDays = List.of(initialDays);