From 87c47a74ce465150478bc8d0d28c1ab17c1737da Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 24 Nov 2024 11:17:54 +0300 Subject: [PATCH] push calling create scene --- .../common/text_field/custom_text_field.dart | 18 +- .../view/device_managment_page.dart | 89 ++++--- .../widgets/device_search_filters.dart | 10 +- .../bloc/routine_bloc/routine_bloc.dart | 103 +++++++- .../bloc/routine_bloc/routine_event.dart | 20 ++ .../bloc/routine_bloc/routine_state.dart | 10 + .../routiens/helper/save_routine_helper.dart | 132 ++++++++++ lib/pages/routiens/helper/setting_helper.dart | 191 ++++++++++----- .../create_scene/create_scene_model.dart | 230 ++++++++++++++++++ .../widgets/routine_search_and_buttons.dart | 30 ++- .../widgets/search_bar_condition_title.dart | 5 +- lib/services/routines_api.dart | 33 +-- lib/utils/constants/api_const.dart | 19 +- 13 files changed, 759 insertions(+), 131 deletions(-) create mode 100644 lib/pages/routiens/helper/save_routine_helper.dart create mode 100644 lib/pages/routiens/models/create_scene/create_scene_model.dart diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index 23f033b6..250ff110 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -18,6 +18,7 @@ class StatefulTextField extends StatefulWidget { this.padding, this.icon, this.hintColor, + required this.onChanged, }); final String title; @@ -32,6 +33,7 @@ class StatefulTextField extends StatefulWidget { final double? padding; final IconData? icon; final Color? hintColor; + final Function(String)? onChanged; @override State createState() => _StatefulTextFieldState(); @@ -59,6 +61,7 @@ class _StatefulTextFieldState extends State { padding: widget.padding, icon: widget.icon, hintColor: widget.hintColor, + onChanged: widget.onChanged, ); } } @@ -78,6 +81,7 @@ class CustomTextField extends StatelessWidget { this.padding, this.icon, this.hintColor, + required this.onChanged, }); final String title; @@ -92,6 +96,7 @@ class CustomTextField extends StatelessWidget { final double? padding; final IconData? icon; final Color? hintColor; + final Function(String)? onChanged; @override Widget build(BuildContext context) { @@ -120,14 +125,21 @@ class CustomTextField extends StatelessWidget { style: const TextStyle(color: Colors.black), decoration: InputDecoration( hintText: hintText, - hintStyle: TextStyle(fontSize: 12, color: hintColor ?? ColorsManager.blackColor), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: padding ?? 10), + hintStyle: TextStyle( + fontSize: 12, color: hintColor ?? ColorsManager.blackColor), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: padding ?? 10), border: InputBorder.none, - suffixIcon: icon != null ? Icon(icon, color: ColorsManager.greyColor) : null, + suffixIcon: icon != null + ? Icon(icon, color: ColorsManager.greyColor) + : null, ), onFieldSubmitted: (_) { onSubmittedFun!(); }, + onChanged: (value) { + onChanged!(value); + }, ), ), ), diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index e0661b50..ffc57131 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; +import 'package:syncrow_web/pages/routiens/view/routines_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -20,7 +22,8 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { create: (context) => DeviceManagementBloc()..add(FetchDevices()), ), BlocProvider( - create: (context) => SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), + create: (context) => + SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), ), ], child: WebScaffold( @@ -30,7 +33,8 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: Theme.of(context).textTheme.headlineLarge, ), ), - centerBody: BlocBuilder(builder: (context, state) { + centerBody: BlocBuilder( + builder: (context, state) { return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -39,16 +43,21 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { backgroundColor: null, ), onPressed: () { - context.read().add(const TriggerSwitchTabsEvent(false)); + context + .read() + .add(const TriggerSwitchTabsEvent(false)); }, child: Text( 'Devices', style: context.textTheme.titleMedium?.copyWith( - color: state is SelectedTabState && state.selectedTab == false - ? ColorsManager.whiteColors - : ColorsManager.grayColor, - fontWeight: - (state is SelectedTabState) && state.selectedTab == false ? FontWeight.w700 : FontWeight.w400, + color: + state is SelectedTabState && state.selectedTab == false + ? ColorsManager.whiteColors + : ColorsManager.grayColor, + fontWeight: (state is SelectedTabState) && + state.selectedTab == false + ? FontWeight.w700 + : FontWeight.w400, ), ), ), @@ -57,16 +66,21 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { backgroundColor: null, ), onPressed: () { - context.read().add(const TriggerSwitchTabsEvent(true)); + context + .read() + .add(const TriggerSwitchTabsEvent(true)); }, child: Text( 'Routines', style: context.textTheme.titleMedium?.copyWith( - color: (state is SelectedTabState) && state.selectedTab == true - ? ColorsManager.whiteColors - : ColorsManager.grayColor, + color: + (state is SelectedTabState) && state.selectedTab == true + ? ColorsManager.whiteColors + : ColorsManager.grayColor, fontWeight: - (state is SelectedTabState) && state.selectedTab == true ? FontWeight.w700 : FontWeight.w400, + (state is SelectedTabState) && state.selectedTab == true + ? FontWeight.w700 + : FontWeight.w400, ), ), ), @@ -74,30 +88,31 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); }), rightBody: const NavigateHomeGridView(), - scaffoldBody: CreateNewRoutineView(), - // BlocBuilder(builder: (context, state) { - // if (state is SelectedTabState && state.selectedTab) { - // return const RoutinesView(); - // } - // if (state is ShowCreateRoutineState && state.showCreateRoutine) { - // return const CreateNewRoutineView(); - // } - // - // return BlocBuilder( - // builder: (context, deviceState) { - // if (deviceState is DeviceManagementLoading) { - // return const Center(child: CircularProgressIndicator()); - // } else if (deviceState is DeviceManagementLoaded || deviceState is DeviceManagementFiltered) { - // final devices = - // (deviceState as dynamic).devices ?? (deviceState as DeviceManagementFiltered).filteredDevices; - // - // return DeviceManagementBody(devices: devices); - // } else { - // return const Center(child: Text('Error fetching Devices')); - // } - // }, - // ); - // }), + scaffoldBody: BlocBuilder( + builder: (context, state) { + if (state is SelectedTabState && state.selectedTab) { + return const RoutinesView(); + } + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } + + return BlocBuilder( + builder: (context, deviceState) { + if (deviceState is DeviceManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (deviceState is DeviceManagementLoaded || + deviceState is DeviceManagementFiltered) { + final devices = (deviceState as dynamic).devices ?? + (deviceState as DeviceManagementFiltered).filteredDevices; + + return DeviceManagementBody(devices: devices); + } else { + return const Center(child: Text('Error fetching Devices')); + } + }, + ); + }), ), ); } diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index d9e47aa6..e3bec220 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -12,7 +12,8 @@ class DeviceSearchFilters extends StatefulWidget { State createState() => _DeviceSearchFiltersState(); } -class _DeviceSearchFiltersState extends State with HelperResponsiveLayout { +class _DeviceSearchFiltersState extends State + with HelperResponsiveLayout { final TextEditingController communityController = TextEditingController(); final TextEditingController unitNameController = TextEditingController(); final TextEditingController productNameController = TextEditingController(); @@ -34,7 +35,8 @@ class _DeviceSearchFiltersState extends State with HelperRe const SizedBox(width: 20), _buildSearchField("Space Name", unitNameController, 200), const SizedBox(width: 20), - _buildSearchField("Device Name / Product Name", productNameController, 300), + _buildSearchField( + "Device Name / Product Name", productNameController, 300), const SizedBox(width: 20), _buildSearchResetButtons(), ], @@ -59,7 +61,8 @@ class _DeviceSearchFiltersState extends State with HelperRe ); } - Widget _buildSearchField(String title, TextEditingController controller, double width) { + Widget _buildSearchField( + String title, TextEditingController controller, double width) { return Container( child: StatefulTextField( title: title, @@ -73,6 +76,7 @@ class _DeviceSearchFiltersState extends State with HelperRe community: communityController.text, searchField: true)); }, + onChanged: (p0) {}, ), ); } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 197da85f..1490b12d 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/routines_api.dart'; @@ -20,6 +23,9 @@ class RoutineBloc extends Bloc { on(_onLoadScenes); on(_onLoadAutomation); on(_onAddFunctionsToRoutine); + on(_onSearchRoutines); + on(_onAddSelectedIcon); + on(_onCreateScene); // on(_onRemoveFunction); // on(_onClearFunctions); } @@ -40,8 +46,6 @@ class RoutineBloc extends Bloc { void _onAddFunctionsToRoutine( AddFunctionToRoutine event, Emitter emit) { - debugPrint('Adding functions to routine: ${event.functions}'); - debugPrint('Unique Custom ID: ${event.uniqueCustomId}'); final currentSelectedFunctions = Map>.from(state.selectedFunctions); @@ -52,7 +56,6 @@ class RoutineBloc extends Bloc { } emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); - debugPrint('Updated selected functions: $currentSelectedFunctions'); } // void _onRemoveFunction(RemoveFunction event, Emitter emit) { @@ -80,7 +83,7 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: 'Failed to load scenes', )); } } @@ -98,7 +101,97 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: 'Failed to load automations', + )); + } + } + + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) { + emit(state.copyWith(routineName: event.query)); + } + + FutureOr _onAddSelectedIcon( + AddSelectedIcon event, Emitter emit) { + emit(state.copyWith(selectedIcon: event.icon)); + } + + bool _isFirstActionDelay(List> actions) { + if (actions.isEmpty) return false; + return actions.first['deviceId'] == 'delay'; + } + + Future _onCreateScene( + CreateSceneEvent event, Emitter emit) async { + try { + // Check if first action is delay + if (_isFirstActionDelay(state.thenItems)) { + emit(state.copyWith( + errorMessage: 'Cannot have delay as the first action', + isLoading: false, + )); + return; + } + + emit(state.copyWith(isLoading: true)); + + final actions = state.thenItems + .map((item) { + final functions = + state.selectedFunctions[item['uniqueCustomId']] ?? []; + if (functions.isEmpty) return null; + + final function = functions.first; + if (item['deviceId'] == 'delay') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'delay', + executorProperty: CreateSceneExecutorProperty( + functionCode: '', + functionValue: '', + delaySeconds: function.value, + ), + ); + } + + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + executorProperty: CreateSceneExecutorProperty( + functionCode: function.functionCode.toString(), + functionValue: function.value, + delaySeconds: 0, + ), + ); + }) + .whereType() + .toList(); + + final createSceneModel = CreateSceneModel( + spaceUuid: spaceId, + iconId: state.selectedIcon ?? '', + showInDevice: true, + sceneName: state.routineName ?? '', + decisionExpr: 'and', + actions: actions, + ); + + final result = await SceneApi.createScene(createSceneModel); + if (result['success']) { + emit(state.copyWith( + isLoading: false, + errorMessage: null, + )); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: result['message'], + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: e.toString(), )); } } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 9e7c6f16..6168748d 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -59,4 +59,24 @@ class RemoveFunction extends RoutineEvent { List get props => [function]; } +class SearchRoutines extends RoutineEvent { + final String query; + const SearchRoutines(this.query); + @override + List get props => [query]; +} + +class AddSelectedIcon extends RoutineEvent { + final String icon; + const AddSelectedIcon(this.icon); + @override + List get props => [icon]; +} + +class CreateSceneEvent extends RoutineEvent { + const CreateSceneEvent(); + @override + List get props => []; +} + class ClearFunctions extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index ce6fea15..911992dc 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -9,6 +9,8 @@ class RoutineState extends Equatable { final Map> selectedFunctions; final bool isLoading; final String? errorMessage; + final String? routineName; + final String? selectedIcon; const RoutineState({ this.ifItems = const [], @@ -19,6 +21,8 @@ class RoutineState extends Equatable { this.selectedFunctions = const {}, this.isLoading = false, this.errorMessage, + this.routineName, + this.selectedIcon, }); RoutineState copyWith({ @@ -29,6 +33,8 @@ class RoutineState extends Equatable { Map>? selectedFunctions, bool? isLoading, String? errorMessage, + String? routineName, + String? selectedIcon, }) { return RoutineState( ifItems: ifItems ?? this.ifItems, @@ -38,6 +44,8 @@ class RoutineState extends Equatable { selectedFunctions: selectedFunctions ?? this.selectedFunctions, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage ?? this.errorMessage, + routineName: routineName ?? this.routineName, + selectedIcon: selectedIcon ?? this.selectedIcon, ); } @@ -50,5 +58,7 @@ class RoutineState extends Equatable { selectedFunctions, isLoading, errorMessage, + routineName, + selectedIcon, ]; } diff --git a/lib/pages/routiens/helper/save_routine_helper.dart b/lib/pages/routiens/helper/save_routine_helper.dart new file mode 100644 index 00000000..69329992 --- /dev/null +++ b/lib/pages/routiens/helper/save_routine_helper.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SaveRoutineHelper { + static Future showSaveRoutineDialog(BuildContext context) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: 600, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader('Create a scene: ${state.routineName ?? ""}'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left side - IF + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'IF:', + style: TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 8), + if (context.read().isTabToRun) + ListTile( + leading: SvgPicture.asset( + Assets.tabToRun, + width: 24, + height: 24, + ), + title: const Text('Tab to run'), + ), + ], + ), + ), + const SizedBox(width: 16), + // Right side - THEN items + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'THEN:', + style: TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 8), + ...state.thenItems.map((item) { + final functions = state.selectedFunctions[ + item['uniqueCustomId']] ?? + []; + return ListTile( + leading: SvgPicture.asset( + item['imagePath'], + width: 22, + height: 22, + ), + title: Text(item['title'], + style: const TextStyle(fontSize: 14)), + subtitle: Wrap( + children: functions + .map((f) => Text( + '${f.operationName}: ${f.value}, ', + style: const TextStyle( + color: + ColorsManager.grayColor, + fontSize: 8), + overflow: TextOverflow.ellipsis, + maxLines: 3, + )) + .toList(), + ), + ); + }), + ], + ), + ), + ], + ), + ), + if (state.errorMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + state.errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ), + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: () { + context + .read() + .add(const CreateSceneEvent()); + Navigator.pop(context); + }, + isConfirmEnabled: true, + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/helper/setting_helper.dart b/lib/pages/routiens/helper/setting_helper.dart index ae8a1bc8..2c0cee77 100644 --- a/lib/pages/routiens/helper/setting_helper.dart +++ b/lib/pages/routiens/helper/setting_helper.dart @@ -10,13 +10,16 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:flutter/cupertino.dart'; class SettingHelper { - static Future?> showSettingDialog( - {required BuildContext context, String? iconId, required bool isAutomation}) async { - return showDialog?>( + static Future showSettingDialog( + {required BuildContext context, + String? iconId, + required bool isAutomation}) async { + return showDialog( context: context, builder: (BuildContext context) { return BlocProvider( - create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), + create: (_) => + SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), child: AlertDialog( contentPadding: EdgeInsets.zero, content: BlocBuilder( @@ -29,7 +32,9 @@ class SettingHelper { } return Container( width: context.read().isExpanded ? 800 : 400, - height: context.read().isExpanded && isAutomation ? 500 : 300, + height: context.read().isExpanded && isAutomation + ? 500 + : 300, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), @@ -52,13 +57,18 @@ class SettingHelper { children: [ Container( padding: const EdgeInsets.only( - top: 10, left: 10, right: 10, bottom: 10), + top: 10, + left: 10, + right: 10, + bottom: 10), child: Column( children: [ InkWell( onTap: () {}, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Validity', @@ -66,13 +76,18 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight + .w400, fontSize: 14), ), const Icon( - Icons.arrow_forward_ios_outlined, - color: ColorsManager.textGray, + Icons + .arrow_forward_ios_outlined, + color: ColorsManager + .textGray, size: 15, ) ], @@ -89,14 +104,18 @@ class SettingHelper { ), InkWell( onTap: () { - BlocProvider.of(context).add( - FetchIcons( + BlocProvider.of( + context) + .add(FetchIcons( expanded: !context - .read() + .read< + SettingBloc>() .isExpanded)); }, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Effective Period', @@ -104,13 +123,18 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight + .w400, fontSize: 14), ), const Icon( - Icons.arrow_forward_ios_outlined, - color: ColorsManager.textGray, + Icons + .arrow_forward_ios_outlined, + color: ColorsManager + .textGray, size: 15, ) ], @@ -126,7 +150,9 @@ class SettingHelper { height: 5, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Executed by', @@ -134,8 +160,10 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight.w400, fontSize: 14), ), Text('Cloud', @@ -143,8 +171,12 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textGray, - fontWeight: FontWeight.w400, + color: + ColorsManager + .textGray, + fontWeight: + FontWeight + .w400, fontSize: 14)), ], ), @@ -157,19 +189,26 @@ class SettingHelper { children: [ Container( padding: const EdgeInsets.only( - top: 10, left: 10, right: 10, bottom: 10), + top: 10, + left: 10, + right: 10, + bottom: 10), child: Column( children: [ InkWell( onTap: () { - BlocProvider.of(context).add( - FetchIcons( + BlocProvider.of( + context) + .add(FetchIcons( expanded: !context - .read() + .read< + SettingBloc>() .isExpanded)); }, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Icons', @@ -177,13 +216,18 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight + .w400, fontSize: 14), ), const Icon( - Icons.arrow_forward_ios_outlined, - color: ColorsManager.textGray, + Icons + .arrow_forward_ios_outlined, + color: ColorsManager + .textGray, size: 15, ) ], @@ -199,7 +243,9 @@ class SettingHelper { height: 5, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Show on devices page', @@ -207,17 +253,21 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight.w400, fontSize: 14), ), Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, children: [ Container( height: 30, width: 1, - color: ColorsManager.graysColor, + color: ColorsManager + .graysColor, ), Transform.scale( scale: .8, @@ -241,7 +291,9 @@ class SettingHelper { height: 5, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Executed by', @@ -249,8 +301,10 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight.w400, fontSize: 14), ), Text('Cloud', @@ -258,8 +312,12 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textGray, - fontWeight: FontWeight.w400, + color: + ColorsManager + .textGray, + fontWeight: + FontWeight + .w400, fontSize: 14)), ], ), @@ -268,12 +326,14 @@ class SettingHelper { ], ), ), - if (context.read().isExpanded && !isAutomation) + if (context.read().isExpanded && + !isAutomation) SizedBox( width: 400, height: 150, child: state is LoadingState - ? const Center(child: CircularProgressIndicator()) + ? const Center( + child: CircularProgressIndicator()) : GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -290,7 +350,8 @@ class SettingHelper { height: 35, child: InkWell( onTap: () { - BlocProvider.of(context) + BlocProvider.of( + context) .add(SelectIcon( iconId: iconModel.uuid, )); @@ -299,12 +360,16 @@ class SettingHelper { child: SizedBox( child: ClipOval( child: Container( - padding: const EdgeInsets.all(1), + padding: + const EdgeInsets.all(1), decoration: BoxDecoration( border: Border.all( - color: selectedIcon == iconModel.uuid - ? ColorsManager.primaryColorWithOpacity - : Colors.transparent, + color: selectedIcon == + iconModel.uuid + ? ColorsManager + .primaryColorWithOpacity + : Colors + .transparent, width: 2, ), shape: BoxShape.circle, @@ -319,8 +384,12 @@ class SettingHelper { ); }, )), - if (context.read().isExpanded && isAutomation) - const SizedBox(height: 350, width: 400, child: EffectivePeriodView()) + if (context.read().isExpanded && + isAutomation) + const SizedBox( + height: 350, + width: 400, + child: EffectivePeriodView()) ], ), Container( @@ -344,14 +413,20 @@ class SettingHelper { alignment: AlignmentDirectional.center, child: Text( 'Cancel', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( color: ColorsManager.textGray, ), ), ), ), ), - Container(width: 1, height: 50, color: ColorsManager.greyColor), + Container( + width: 1, + height: 50, + color: ColorsManager.greyColor), Expanded( child: InkWell( onTap: () { @@ -361,8 +436,12 @@ class SettingHelper { alignment: AlignmentDirectional.center, child: Text( 'Confirm', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager + .primaryColorWithOpacity, ), ), ), diff --git a/lib/pages/routiens/models/create_scene/create_scene_model.dart b/lib/pages/routiens/models/create_scene/create_scene_model.dart new file mode 100644 index 00000000..c669aa9a --- /dev/null +++ b/lib/pages/routiens/models/create_scene/create_scene_model.dart @@ -0,0 +1,230 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +class CreateSceneModel { + String spaceUuid; + String iconId; + bool showInDevice; + String sceneName; + String decisionExpr; + List actions; + + CreateSceneModel({ + required this.spaceUuid, + required this.iconId, + required this.showInDevice, + required this.sceneName, + required this.decisionExpr, + required this.actions, + }); + + CreateSceneModel copyWith({ + String? spaceUuid, + String? iconId, + bool? showInDevice, + String? sceneName, + String? decisionExpr, + List? actions, + bool? showInHomePage, + }) { + return CreateSceneModel( + spaceUuid: spaceUuid ?? this.spaceUuid, + iconId: iconId ?? this.iconId, + showInDevice: showInDevice ?? this.showInDevice, + sceneName: sceneName ?? this.sceneName, + decisionExpr: decisionExpr ?? this.decisionExpr, + actions: actions ?? this.actions, + ); + } + + Map toMap([String? sceneId]) { + return { + if (sceneId == null) 'spaceUuid': spaceUuid, + if (iconId.isNotEmpty) 'iconUuid': iconId, + 'showInHomePage': showInDevice, + 'sceneName': sceneName, + 'decisionExpr': decisionExpr, + 'actions': actions.map((x) => x.toMap()).toList(), + }; + } + + factory CreateSceneModel.fromMap(Map map) { + return CreateSceneModel( + spaceUuid: map['spaceUuid'] ?? '', + showInDevice: map['showInHomePage'] ?? false, + iconId: map['iconUuid'] ?? '', + sceneName: map['sceneName'] ?? '', + decisionExpr: map['decisionExpr'] ?? '', + actions: List.from( + map['actions']?.map((x) => CreateSceneAction.fromMap(x))), + ); + } + + String toJson([String? sceneId]) => json.encode(toMap(sceneId)); + + factory CreateSceneModel.fromJson(String source) => + CreateSceneModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'CreateSceneModel(unitUuid: $spaceUuid, sceneName: $sceneName, decisionExpr: $decisionExpr, actions: $actions)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneModel && + other.spaceUuid == spaceUuid && + other.iconId == iconId && + other.showInDevice == showInDevice && + other.sceneName == sceneName && + other.decisionExpr == decisionExpr && + listEquals(other.actions, actions); + } + + @override + int get hashCode { + return spaceUuid.hashCode ^ + sceneName.hashCode ^ + decisionExpr.hashCode ^ + actions.hashCode; + } +} + +class CreateSceneAction { + String entityId; + String actionExecutor; + CreateSceneExecutorProperty? executorProperty; + + CreateSceneAction({ + required this.entityId, + required this.actionExecutor, + required this.executorProperty, + }); + + CreateSceneAction copyWith({ + String? entityId, + String? actionExecutor, + CreateSceneExecutorProperty? executorProperty, + }) { + return CreateSceneAction( + entityId: entityId ?? this.entityId, + actionExecutor: actionExecutor ?? this.actionExecutor, + executorProperty: executorProperty ?? this.executorProperty, + ); + } + + Map toMap() { + if (executorProperty != null) { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'executorProperty': executorProperty?.toMap(actionExecutor), + }; + } else { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + }; + } + } + + factory CreateSceneAction.fromMap(Map map) { + return CreateSceneAction( + entityId: map['entityId'] ?? '', + actionExecutor: map['actionExecutor'] ?? '', + executorProperty: + CreateSceneExecutorProperty.fromMap(map['executorProperty']), + ); + } + + String toJson() => json.encode(toMap()); + + factory CreateSceneAction.fromJson(String source) => + CreateSceneAction.fromMap(json.decode(source)); + + @override + String toString() => + 'CreateSceneAction(entityId: $entityId, actionExecutor: $actionExecutor, executorProperty: $executorProperty)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneAction && + other.entityId == entityId && + other.actionExecutor == actionExecutor && + other.executorProperty == executorProperty; + } + + @override + int get hashCode => + entityId.hashCode ^ actionExecutor.hashCode ^ executorProperty.hashCode; +} + +class CreateSceneExecutorProperty { + String functionCode; + dynamic functionValue; + int delaySeconds; + + CreateSceneExecutorProperty({ + required this.functionCode, + required this.functionValue, + required this.delaySeconds, + }); + + CreateSceneExecutorProperty copyWith({ + String? functionCode, + dynamic functionValue, + int? delaySeconds, + }) { + return CreateSceneExecutorProperty( + functionCode: functionCode ?? this.functionCode, + functionValue: functionValue ?? this.functionValue, + delaySeconds: delaySeconds ?? this.delaySeconds, + ); + } + + Map toMap(String actionExecutor) { + final map = {}; + if (functionCode.isNotEmpty) map['functionCode'] = functionCode; + if (functionValue != null) map['functionValue'] = functionValue; + if (actionExecutor == 'delay' && delaySeconds > 0) { + map['delaySeconds'] = delaySeconds; + } + return map; + } + + factory CreateSceneExecutorProperty.fromMap(Map map) { + return CreateSceneExecutorProperty( + functionCode: map['functionCode'] ?? '', + functionValue: map['functionValue'] ?? '', + delaySeconds: map['delaySeconds']?.toInt() ?? 0, + ); + } + + String toJson(String actionExecutor) => json.encode(toMap(actionExecutor)); + + factory CreateSceneExecutorProperty.fromJson(String source) => + CreateSceneExecutorProperty.fromMap(json.decode(source)); + + @override + String toString() => + 'CreateSceneExecutorProperty(functionCode: $functionCode, functionValue: $functionValue, delaySeconds: $delaySeconds)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneExecutorProperty && + other.functionCode == functionCode && + other.functionValue == functionValue && + other.delaySeconds == delaySeconds; + } + + @override + int get hashCode => + functionCode.hashCode ^ functionValue.hashCode ^ delaySeconds.hashCode; +} diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 598905e1..66f2a3a0 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/main.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/save_routine_helper.dart'; import 'package:syncrow_web/pages/routiens/helper/setting_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -28,7 +32,9 @@ class RoutineSearchAndButtons extends StatelessWidget { children: [ ConstrainedBox( constraints: BoxConstraints( - maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), + maxWidth: constraints.maxWidth > 700 + ? 450 + : constraints.maxWidth - 32), child: StatefulTextField( title: 'Routine Name', height: 40, @@ -38,6 +44,11 @@ class RoutineSearchAndButtons extends StatelessWidget { elevation: 0, borderRadius: 15, width: 450, + onChanged: (value) { + context + .read() + .add(SearchRoutines(value)); + }, ), ), (constraints.maxWidth <= 1000) @@ -48,8 +59,17 @@ class RoutineSearchAndButtons extends StatelessWidget { child: Center( child: DefaultButton( onPressed: () async { - final result = await SettingHelper.showSettingDialog( - context: context, isAutomation: true); + final result = + await SettingHelper.showSettingDialog( + context: context, + isAutomation: context + .read() + .isAutomation); + if (result != null) { + context + .read() + .add(AddSelectedIcon(result)); + } }, borderRadius: 15, elevation: 0, @@ -100,7 +120,9 @@ class RoutineSearchAndButtons extends StatelessWidget { width: 200, child: Center( child: DefaultButton( - onPressed: () {}, + onPressed: () { + SaveRoutineHelper.showSaveRoutineDialog(context); + }, borderRadius: 15, elevation: 0, backgroundColor: ColorsManager.primaryColor, diff --git a/lib/pages/routiens/widgets/search_bar_condition_title.dart b/lib/pages/routiens/widgets/search_bar_condition_title.dart index 40d6b575..e36ba873 100644 --- a/lib/pages/routiens/widgets/search_bar_condition_title.dart +++ b/lib/pages/routiens/widgets/search_bar_condition_title.dart @@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLayout { +class ConditionTitleAndSearchBar extends StatelessWidget + with HelperResponsiveLayout { const ConditionTitleAndSearchBar({ super.key, }); @@ -33,6 +34,7 @@ class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLa borderRadius: BorderRadius.circular(15), ), controller: TextEditingController(), + onChanged: (value) {}, ), ], ) @@ -55,6 +57,7 @@ class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLa borderRadius: BorderRadius.circular(15), ), controller: TextEditingController(), + onChanged: (value) {}, ), ], ); diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index 7b001751..b137982d 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -7,22 +8,22 @@ class SceneApi { static final HTTPService _httpService = HTTPService(); // //create scene -// static Future> createScene( -// CreateSceneModel createSceneModel) async { -// try { -// final response = await _httpService.post( -// path: ApiEndpoints.createScene, -// body: createSceneModel.toMap(), -// showServerMessage: false, -// expectedResponseModel: (json) { -// return json; -// }, -// ); -// return response; -// } catch (e) { -// rethrow; -// } -// } + static Future> createScene( + CreateSceneModel createSceneModel) async { + try { + final response = await _httpService.post( + path: ApiEndpoints.createScene, + body: createSceneModel.toMap(), + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } // // // create automation // static Future> createAutomation( diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index c7f7bccb..a5decc3b 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -11,12 +11,14 @@ abstract class ApiEndpoints { static const String visitorPassword = '/visitor-password'; static const String getDevices = '/visitor-password/devices'; - static const String sendOnlineOneTime = '/visitor-password/temporary-password/online/one-time'; + static const String sendOnlineOneTime = + '/visitor-password/temporary-password/online/one-time'; static const String sendOnlineMultipleTime = '/visitor-password/temporary-password/online/multiple-time'; //offline Password - static const String sendOffLineOneTime = '/visitor-password/temporary-password/offline/one-time'; + static const String sendOffLineOneTime = + '/visitor-password/temporary-password/offline/one-time'; static const String sendOffLineMultipleTime = '/visitor-password/temporary-password/offline/multiple-time'; @@ -38,13 +40,18 @@ abstract class ApiEndpoints { '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; - static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}'; - static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; - static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/device/factory/reset/{deviceUuid}'; - static const String powerClamp = '/device/{powerClampUuid}/power-clamp/status'; + static const String powerClamp = + '/device/{powerClampUuid}/power-clamp/status'; static const String getSpaceScenes = '/scene/tap-to-run/{unitUuid}'; static const String getSpaceAutomation = '/automation/{unitUuid}'; static const String getIconScene = '/scene/icon'; + static const String createScene = '/scene/tap-to-run'; }