diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b87628bd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "automations" + ] +} \ No newline at end of file diff --git a/assets/icons/functions_icons/ac_cooling.svg b/assets/icons/functions_icons/ac_cooling.svg new file mode 100644 index 00000000..e95c0d4e --- /dev/null +++ b/assets/icons/functions_icons/ac_cooling.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_auto.svg b/assets/icons/functions_icons/ac_fan_auto.svg new file mode 100644 index 00000000..0acacfef --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_auto.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_high.svg b/assets/icons/functions_icons/ac_fan_high.svg new file mode 100644 index 00000000..d6131531 --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_high.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_low.svg b/assets/icons/functions_icons/ac_fan_low.svg new file mode 100644 index 00000000..f4bf56b7 --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_low.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_middle.svg b/assets/icons/functions_icons/ac_fan_middle.svg new file mode 100644 index 00000000..ee940238 --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_middle.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_heating.svg b/assets/icons/functions_icons/ac_heating.svg new file mode 100644 index 00000000..47a160c8 --- /dev/null +++ b/assets/icons/functions_icons/ac_heating.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_power.svg b/assets/icons/functions_icons/ac_power.svg new file mode 100644 index 00000000..cc2127f0 --- /dev/null +++ b/assets/icons/functions_icons/ac_power.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/ac_power_off.svg b/assets/icons/functions_icons/ac_power_off.svg new file mode 100644 index 00000000..70f7f9aa --- /dev/null +++ b/assets/icons/functions_icons/ac_power_off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/automation_functions/card_unlock.svg b/assets/icons/functions_icons/automation_functions/card_unlock.svg new file mode 100644 index 00000000..dd77680a --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/card_unlock.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/current_temp.svg b/assets/icons/functions_icons/automation_functions/current_temp.svg new file mode 100644 index 00000000..42cceb23 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/current_temp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/doorbell.svg b/assets/icons/functions_icons/automation_functions/doorbell.svg new file mode 100644 index 00000000..1dc515a9 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/doorbell.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg b/assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg new file mode 100644 index 00000000..8f4a5901 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/double_lock.svg b/assets/icons/functions_icons/automation_functions/double_lock.svg new file mode 100644 index 00000000..d8ad971d --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/double_lock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg b/assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg new file mode 100644 index 00000000..f9f5b84c --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/hijack_alarm.svg b/assets/icons/functions_icons/automation_functions/hijack_alarm.svg new file mode 100644 index 00000000..e32997fb --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/hijack_alarm.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/lock_alarm.svg b/assets/icons/functions_icons/automation_functions/lock_alarm.svg new file mode 100644 index 00000000..8bd2deeb --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/lock_alarm.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/motion.svg b/assets/icons/functions_icons/automation_functions/motion.svg new file mode 100644 index 00000000..8d69463b --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/motion.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/password_unlock.svg b/assets/icons/functions_icons/automation_functions/password_unlock.svg new file mode 100644 index 00000000..1920b69f --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/password_unlock.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/presence.svg b/assets/icons/functions_icons/automation_functions/presence.svg new file mode 100644 index 00000000..d71a474d --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/presence.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/presence_state.svg b/assets/icons/functions_icons/automation_functions/presence_state.svg new file mode 100644 index 00000000..d5de48e1 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/presence_state.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/remote_unlock_req.svg b/assets/icons/functions_icons/automation_functions/remote_unlock_req.svg new file mode 100644 index 00000000..da128ff7 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/remote_unlock_req.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg b/assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg new file mode 100644 index 00000000..39fc859b --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/residual_electricity.svg b/assets/icons/functions_icons/automation_functions/residual_electricity.svg new file mode 100644 index 00000000..6a5b6127 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/residual_electricity.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/functions_icons/automation_functions/self_test_result.svg b/assets/icons/functions_icons/automation_functions/self_test_result.svg new file mode 100644 index 00000000..8739327b --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/self_test_result.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/temp_password_unlock.svg b/assets/icons/functions_icons/automation_functions/temp_password_unlock.svg new file mode 100644 index 00000000..98d7573c --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/temp_password_unlock.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/celsius_degrees.svg b/assets/icons/functions_icons/celsius_degrees.svg new file mode 100644 index 00000000..7acbd6e7 --- /dev/null +++ b/assets/icons/functions_icons/celsius_degrees.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/icons/functions_icons/child_lock.svg b/assets/icons/functions_icons/child_lock.svg new file mode 100644 index 00000000..6b0138bf --- /dev/null +++ b/assets/icons/functions_icons/child_lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/factory_reset.svg b/assets/icons/functions_icons/factory_reset.svg new file mode 100644 index 00000000..7a47f24b --- /dev/null +++ b/assets/icons/functions_icons/factory_reset.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/functions_icons/fan_speed.svg b/assets/icons/functions_icons/fan_speed.svg new file mode 100644 index 00000000..07a48834 --- /dev/null +++ b/assets/icons/functions_icons/fan_speed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/far_detection.svg b/assets/icons/functions_icons/far_detection.svg new file mode 100644 index 00000000..2827d94a --- /dev/null +++ b/assets/icons/functions_icons/far_detection.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/far_detection_function.svg b/assets/icons/functions_icons/far_detection_function.svg new file mode 100644 index 00000000..894b84ed --- /dev/null +++ b/assets/icons/functions_icons/far_detection_function.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/freezing.svg b/assets/icons/functions_icons/freezing.svg new file mode 100644 index 00000000..6c02f2e4 --- /dev/null +++ b/assets/icons/functions_icons/freezing.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/functions_icons/indicator.svg b/assets/icons/functions_icons/indicator.svg new file mode 100644 index 00000000..b58a976e --- /dev/null +++ b/assets/icons/functions_icons/indicator.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/light_countdown.svg b/assets/icons/functions_icons/light_countdown.svg new file mode 100644 index 00000000..94f65b9a --- /dev/null +++ b/assets/icons/functions_icons/light_countdown.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/master_state.svg b/assets/icons/functions_icons/master_state.svg new file mode 100644 index 00000000..0aafae1a --- /dev/null +++ b/assets/icons/functions_icons/master_state.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/motion_detection.svg b/assets/icons/functions_icons/motion_detection.svg new file mode 100644 index 00000000..a9b2d685 --- /dev/null +++ b/assets/icons/functions_icons/motion_detection.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/motionless_detection.svg b/assets/icons/functions_icons/motionless_detection.svg new file mode 100644 index 00000000..25a767c1 --- /dev/null +++ b/assets/icons/functions_icons/motionless_detection.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/functions_icons/nobody_time.svg b/assets/icons/functions_icons/nobody_time.svg new file mode 100644 index 00000000..df80b517 --- /dev/null +++ b/assets/icons/functions_icons/nobody_time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/functions_icons/reset_off.svg b/assets/icons/functions_icons/reset_off.svg new file mode 100644 index 00000000..eac88f2b --- /dev/null +++ b/assets/icons/functions_icons/reset_off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/functions_icons/scene_child_lock.svg b/assets/icons/functions_icons/scene_child_lock.svg new file mode 100644 index 00000000..7e56164a --- /dev/null +++ b/assets/icons/functions_icons/scene_child_lock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/scene_child_unlock.svg b/assets/icons/functions_icons/scene_child_unlock.svg new file mode 100644 index 00000000..4eafbdea --- /dev/null +++ b/assets/icons/functions_icons/scene_child_unlock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/scene_refresh.svg b/assets/icons/functions_icons/scene_refresh.svg new file mode 100644 index 00000000..c54ffb04 --- /dev/null +++ b/assets/icons/functions_icons/scene_refresh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/functions_icons/sensitivity.svg b/assets/icons/functions_icons/sensitivity.svg new file mode 100644 index 00000000..b75ebd3e --- /dev/null +++ b/assets/icons/functions_icons/sensitivity.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/sesitivity_operation_icon.svg b/assets/icons/functions_icons/sesitivity_operation_icon.svg new file mode 100644 index 00000000..612148c5 --- /dev/null +++ b/assets/icons/functions_icons/sesitivity_operation_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/switch_alarm_sound.svg b/assets/icons/functions_icons/switch_alarm_sound.svg new file mode 100644 index 00000000..db645338 --- /dev/null +++ b/assets/icons/functions_icons/switch_alarm_sound.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/functions_icons/tempreture.svg b/assets/icons/functions_icons/tempreture.svg new file mode 100644 index 00000000..448083a7 --- /dev/null +++ b/assets/icons/functions_icons/tempreture.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/routine/automation.svg b/assets/icons/routine/automation.svg new file mode 100644 index 00000000..a67aadaf --- /dev/null +++ b/assets/icons/routine/automation.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/delay.svg b/assets/icons/routine/delay.svg new file mode 100644 index 00000000..49a8d31c --- /dev/null +++ b/assets/icons/routine/delay.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/map.svg b/assets/icons/routine/map.svg new file mode 100644 index 00000000..595a0d8c --- /dev/null +++ b/assets/icons/routine/map.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/notification.svg b/assets/icons/routine/notification.svg new file mode 100644 index 00000000..f196f466 --- /dev/null +++ b/assets/icons/routine/notification.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/routine/schedule.svg b/assets/icons/routine/schedule.svg new file mode 100644 index 00000000..423eb577 --- /dev/null +++ b/assets/icons/routine/schedule.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/tab_to_run.svg b/assets/icons/routine/tab_to_run.svg new file mode 100644 index 00000000..c8660bb8 --- /dev/null +++ b/assets/icons/routine/tab_to_run.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/routine/weather.svg b/assets/icons/routine/weather.svg new file mode 100644 index 00000000..49ed9408 --- /dev/null +++ b/assets/icons/routine/weather.svg @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index c544f227..2040d175 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/utils/app_routes.dart'; @@ -14,7 +15,8 @@ import 'package:syncrow_web/utils/theme/theme.dart'; Future main() async { try { - const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development'); + const environment = + String.fromEnvironment('FLAVOR', defaultValue: 'development'); await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); initialSetup(); @@ -46,10 +48,14 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), + BlocProvider( + create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), - ) + ), + BlocProvider( + create: (context) => RoutineBloc(), + ), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 22baba36..60abc0d2 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index b695da4a..bb574e74 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -1,59 +1,107 @@ import 'package:flutter/material.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'; class StatefulTextField extends StatefulWidget { - const StatefulTextField( - {super.key, - required this.title, - this.hintText = 'Please enter', - required this.width, - this.elevation = 0, - required this.controller, - this.onSubmitted}); + const StatefulTextField({ + super.key, + required this.title, + this.hintText = 'Please enter', + required this.width, + this.elevation, + required this.controller, + this.onSubmitted, + this.boxDecoration, + this.borderRadius, + this.height, + this.padding, + this.icon, + this.hintColor, + required this.onChanged, + this.isRequired, + }); final String title; final String hintText; final double width; - final double elevation; + final double? elevation; final TextEditingController controller; final Function? onSubmitted; + final BoxDecoration? boxDecoration; + final double? borderRadius; + final double? height; + final double? padding; + final IconData? icon; + final Color? hintColor; + final Function(String)? onChanged; + final bool? isRequired; @override State createState() => _StatefulTextFieldState(); } class _StatefulTextFieldState extends State { + @override + void dispose() { + widget.controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Container( - child: CustomTextField( - title: widget.title, - controller: widget.controller, - hintText: widget.hintText, - width: widget.width, - elevation: widget.elevation, - onSubmittedFun: widget.onSubmitted), + return CustomTextField( + title: widget.title, + controller: widget.controller, + hintText: widget.hintText, + width: widget.width, + elevation: widget.elevation, + onSubmittedFun: widget.onSubmitted, + boxDecoration: widget.boxDecoration, + borderRadius: widget.borderRadius, + height: widget.height, + padding: widget.padding, + icon: widget.icon, + hintColor: widget.hintColor, + onChanged: widget.onChanged, + isRequired: widget.isRequired, ); } } class CustomTextField extends StatelessWidget { - const CustomTextField( - {super.key, - required this.title, - required this.controller, - this.hintText = 'Please enter', - required this.width, - this.elevation = 0, - this.onSubmittedFun}); + const CustomTextField({ + super.key, + required this.title, + required this.controller, + this.hintText = 'Please enter', + required this.width, + this.elevation, + this.onSubmittedFun, + this.boxDecoration, + this.borderRadius, + this.height, + this.padding, + this.icon, + this.hintColor, + required this.onChanged, + this.isRequired, + }); final String title; final TextEditingController controller; final String hintText; final double width; - final double elevation; + final double? elevation; final Function? onSubmittedFun; + final BoxDecoration? boxDecoration; + final double? borderRadius; + final double? height; + final double? padding; + final IconData? icon; + final Color? hintColor; + final Function(String)? onChanged; + final bool? isRequired; @override Widget build(BuildContext context) { @@ -61,40 +109,65 @@ class CustomTextField extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - Text( - title, - style: context.textTheme.bodyMedium!.copyWith( - fontSize: 13, - fontWeight: FontWeight.w600, - color: const Color(0xff000000), - ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Visibility( + visible: isRequired == true, + child: Text('* ', + style: context.textTheme.bodyMedium! + .copyWith(color: Colors.red, fontSize: 13)), + ), + Text( + title, + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 13, + fontWeight: FontWeight.w600, + color: const Color(0xff000000), + ), + ), + ], ), const SizedBox(height: 8), Material( - elevation: elevation, - borderRadius: BorderRadius.circular(8), + elevation: elevation ?? 0, + borderRadius: BorderRadius.circular(borderRadius ?? 8), child: Container( width: width, - height: 45, - decoration: containerDecoration, - - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(8), - // ), + height: height ?? 45, + decoration: boxDecoration ?? containerDecoration, child: TextFormField( controller: controller, style: const TextStyle(color: Colors.black), + decoration: InputDecoration( hintText: hintText, - hintStyle: const TextStyle(fontSize: 12), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 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, ), onFieldSubmitted: (_) { onSubmittedFun!(); }, + onChanged: (value) { + onChanged!(value); + }, + + /// required validator + validator: (value) { + if (isRequired == true) { + if (value == null || value.isEmpty) { + return 'This field is required'; + } + } + return null; + }, ), ), ), diff --git a/lib/pages/device_managment/ac/model/ac_model.dart b/lib/pages/device_managment/ac/model/ac_model.dart index 2803e51e..1eb2145f 100644 --- a/lib/pages/device_managment/ac/model/ac_model.dart +++ b/lib/pages/device_managment/ac/model/ac_model.dart @@ -1,8 +1,5 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; - -enum TempModes { hot, cold, wind } - -enum FanSpeeds { auto, low, middle, high } +import 'package:syncrow_web/utils/constants/app_enum.dart'; class AcStatusModel { final String uuid; diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart index 60d48256..81f0685d 100644 --- a/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.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/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class BatchAcMode extends StatelessWidget { @@ -27,15 +27,19 @@ class BatchAcMode extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildIconContainer(context, TempModes.cold, Assets.freezing, value == TempModes.cold), - _buildIconContainer(context, TempModes.hot, Assets.acSun, value == TempModes.hot), - _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, value == TempModes.wind), + _buildIconContainer(context, TempModes.cold, Assets.freezing, + value == TempModes.cold), + _buildIconContainer( + context, TempModes.hot, Assets.acSun, value == TempModes.hot), + _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, + value == TempModes.wind), ], ), ); } - Widget _buildIconContainer(BuildContext context, TempModes mode, String assetPath, bool isSelected) { + Widget _buildIconContainer( + BuildContext context, TempModes mode, String assetPath, bool isSelected) { return Flexible( child: GestureDetector( onTap: () { diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart index ba49047a..4d7eb449 100644 --- a/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.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/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class BatchFanSpeedControl extends StatelessWidget { @@ -30,8 +30,10 @@ class BatchFanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, value == FanSpeeds.auto), - _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, value == FanSpeeds.low), + _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, + value == FanSpeeds.auto), + _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, + value == FanSpeeds.low), ], ), const SizedBox(height: 8), @@ -39,8 +41,10 @@ class BatchFanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, value == FanSpeeds.middle), - _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, value == FanSpeeds.high), + _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, + value == FanSpeeds.middle), + _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, + value == FanSpeeds.high), ], ) ], @@ -48,7 +52,8 @@ class BatchFanSpeedControl extends StatelessWidget { ); } - Widget _buildIconContainer(BuildContext context, FanSpeeds speed, String assetPath, bool isSelected) { + Widget _buildIconContainer(BuildContext context, FanSpeeds speed, + String assetPath, bool isSelected) { return GestureDetector( onTap: () { context.read().add( diff --git a/lib/pages/device_managment/ac/view/control_list/ac_mode.dart b/lib/pages/device_managment/ac/view/control_list/ac_mode.dart index c6ffc052..91ca0f8c 100644 --- a/lib/pages/device_managment/ac/view/control_list/ac_mode.dart +++ b/lib/pages/device_managment/ac/view/control_list/ac_mode.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.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/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class AcMode extends StatelessWidget { @@ -27,15 +27,19 @@ class AcMode extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildIconContainer(context, TempModes.cold, Assets.freezing, value == TempModes.cold), - _buildIconContainer(context, TempModes.hot, Assets.acSun, value == TempModes.hot), - _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, value == TempModes.wind), + _buildIconContainer(context, TempModes.cold, Assets.freezing, + value == TempModes.cold), + _buildIconContainer( + context, TempModes.hot, Assets.acSun, value == TempModes.hot), + _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, + value == TempModes.wind), ], ), ); } - Widget _buildIconContainer(BuildContext context, TempModes mode, String assetPath, bool isSelected) { + Widget _buildIconContainer( + BuildContext context, TempModes mode, String assetPath, bool isSelected) { return Flexible( child: GestureDetector( onTap: () { diff --git a/lib/pages/device_managment/ac/view/control_list/fan_speed.dart b/lib/pages/device_managment/ac/view/control_list/fan_speed.dart index 952e112b..09ca80cb 100644 --- a/lib/pages/device_managment/ac/view/control_list/fan_speed.dart +++ b/lib/pages/device_managment/ac/view/control_list/fan_speed.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.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/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class FanSpeedControl extends StatelessWidget { @@ -29,8 +29,10 @@ class FanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, value == FanSpeeds.auto), - _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, value == FanSpeeds.low), + _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, + value == FanSpeeds.auto), + _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, + value == FanSpeeds.low), ], ), const SizedBox(height: 8), @@ -38,8 +40,10 @@ class FanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, value == FanSpeeds.middle), - _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, value == FanSpeeds.high), + _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, + value == FanSpeeds.middle), + _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, + value == FanSpeeds.high), ], ) ], @@ -47,7 +51,8 @@ class FanSpeedControl extends StatelessWidget { ); } - Widget _buildIconContainer(BuildContext context, FanSpeeds speed, String assetPath, bool isSelected) { + Widget _buildIconContainer(BuildContext context, FanSpeeds speed, + String assetPath, bool isSelected) { return GestureDetector( onTap: () { context.read().add( diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart similarity index 100% rename from lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart rename to lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart similarity index 100% rename from lib/pages/device_managment/all_devices/bloc/device_managment_event.dart rename to lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_state.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_state.dart similarity index 100% rename from lib/pages/device_managment/all_devices/bloc/device_managment_state.dart rename to lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_state.dart diff --git a/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart new file mode 100644 index 00000000..3eaccf70 --- /dev/null +++ b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart @@ -0,0 +1,28 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'switch_tabs_event.dart'; +part 'switch_tabs_state.dart'; + +class SwitchTabsBloc extends Bloc { + SwitchTabsBloc() : super(SwitchTabsInitial()) { + on(_switchTab); + on(_newRoutineView); + } + + FutureOr _switchTab( + TriggerSwitchTabsEvent event, + Emitter emit, + ) { + emit(SelectedTabState(event.isRoutineView)); + } + + FutureOr _newRoutineView( + CreateNewRoutineViewEvent event, + Emitter emit, + ) { + emit(ShowCreateRoutineState(event.showCreateNewRoutineView)); + } +} diff --git a/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart new file mode 100644 index 00000000..98cad361 --- /dev/null +++ b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart @@ -0,0 +1,21 @@ +part of 'switch_tabs_bloc.dart'; + +sealed class SwitchTabsEvent extends Equatable { + const SwitchTabsEvent(); +} + +class TriggerSwitchTabsEvent extends SwitchTabsEvent { + final bool isRoutineView; + const TriggerSwitchTabsEvent(this.isRoutineView); + + @override + List get props => [isRoutineView]; +} + +class CreateNewRoutineViewEvent extends SwitchTabsEvent { + final bool showCreateNewRoutineView; + const CreateNewRoutineViewEvent(this.showCreateNewRoutineView); + + @override + List get props => [showCreateNewRoutineView]; +} diff --git a/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart new file mode 100644 index 00000000..dd01aeaa --- /dev/null +++ b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart @@ -0,0 +1,26 @@ +part of 'switch_tabs_bloc.dart'; + +sealed class SwitchTabsState extends Equatable { + const SwitchTabsState(); +} + +final class SwitchTabsInitial extends SwitchTabsState { + @override + List get props => []; +} + +class SelectedTabState extends SwitchTabsState { + final bool selectedTab; + const SelectedTabState(this.selectedTab); + + @override + List get props => [selectedTab]; +} + +class ShowCreateRoutineState extends SwitchTabsState { + final bool showCreateRoutine; + const ShowCreateRoutineState(this.showCreateRoutine); + + @override + List get props => [showCreateRoutine]; +} diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index df80c3e7..b7e4f010 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -2,7 +2,13 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_com import 'package:syncrow_web/pages/device_managment/all_devices/models/device_space_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/enum/device_types.dart'; class AllDevicesModel { /* @@ -99,6 +105,7 @@ class AllDevicesModel { this.productName, this.spaces, }); + AllDevicesModel.fromJson(Map json) { room = (json['room'] != null && (json['room'] is Map)) ? DevicesModelRoom.fromJson(json['room']) @@ -117,7 +124,7 @@ class AllDevicesModel { categoryName = json['categoryName']?.toString(); createTime = int.tryParse(json['createTime']?.toString() ?? ''); gatewayId = json['gatewayId']?.toString(); - icon = json['icon'] ?? _getDefaultIcon(productType); + icon = json['icon'] ?? getDefaultIcon(productType); ip = json['ip']?.toString(); lat = json['lat']?.toString(); localKey = json['localKey']?.toString(); @@ -132,6 +139,7 @@ class AllDevicesModel { updateTime = int.tryParse(json['updateTime']?.toString() ?? ''); uuid = json['uuid']?.toString(); batteryLevel = int.tryParse(json['battery']?.toString() ?? ''); + productName = json['productName']?.toString(); if (json['spaces'] != null && json['spaces'] is List) { spaces = (json['spaces'] as List) @@ -140,31 +148,138 @@ class AllDevicesModel { } } - String _getDefaultIcon(String? productType) { + String getDefaultIcon(String? productType) { + /* + AC +GD +3G +3G +GW +DL +WPS +CPS +AC +CPS +WPS +GW +AC +CUR +DS +1GT +2GT +3GT +1G +1G +2G +2G +DS +WH +1GT +2GT +3GT +GD +WL +WL +3G +CUR +GW +PC +PC +SOS + + */ + DeviceType type = devicesTypesMap[productType] ?? DeviceType.Other; + String tempIcon = ''; + if (type == DeviceType.LightBulb) { + tempIcon = Assets.lightBulb; + } else if (type == DeviceType.CeilingSensor || + type == DeviceType.WallSensor) { + tempIcon = Assets.sensors; + } else if (type == DeviceType.AC) { + tempIcon = Assets.ac; + } else if (type == DeviceType.DoorLock) { + tempIcon = Assets.doorLock; + } else if (type == DeviceType.Curtain) { + tempIcon = Assets.curtain; + } else if (type == DeviceType.ThreeGang) { + tempIcon = Assets.gangSwitch; + } else if (type == DeviceType.Gateway) { + tempIcon = Assets.gateway; + } else if (type == DeviceType.OneGang) { + tempIcon = Assets.oneGang; + } else if (type == DeviceType.TwoGang) { + tempIcon = Assets.twoGang; + } else if (type == DeviceType.WH) { + tempIcon = Assets.waterHeater; + } else if (type == DeviceType.DS) { + // tempIcon = Assets.mainDoor; + } else if (type == DeviceType.OneTouch) { + // tempIcon = Assets.oneGang; + } else if (type == DeviceType.TowTouch) { + // tempIcon = Assets.twoGang; + } else if (type == DeviceType.GarageDoor) { + //tempIcon = Assets.; + } else if (type == DeviceType.ThreeTouch) { + // tempIcon = Assets.gang3touch; + } else if (type == DeviceType.WaterLeak) { + tempIcon = Assets.waterLeakNormal; + } else { + tempIcon = Assets.logoHorizontal; + } + return tempIcon; + } + + List get functions { + return _getDeviceFunctions(); + } + + //! Functions for Devices Types + List _getDeviceFunctions() { switch (productType) { - case 'LightBulb': - return Assets.lightBulb; - case 'CeilingSensor': - case 'WallSensor': - return Assets.sensors; case 'AC': - return Assets.ac; - case 'DoorLock': - return Assets.doorLock; - case 'Curtain': - return Assets.curtain; - case '3G': - case '2G': + return [ + SwitchFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + ModeFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + TempSetFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + LevelFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + ChildLockFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + case '1G': - return Assets.gangSwitch; - case 'Gateway': - return Assets.gateway; - case 'WH': - return Assets.blackLogo; - case 'DS': - return Assets.sensors; + return [ + OneGangSwitchFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + OneGangCountdownFunction( + deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + + case '2G': + return [ + TwoGangSwitch1Function(deviceId: uuid ?? '', deviceName: name ?? ''), + TwoGangSwitch2Function(deviceId: uuid ?? '', deviceName: name ?? ''), + TwoGangCountdown1Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + TwoGangCountdown2Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + + case '3G': + return [ + ThreeGangSwitch1Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangSwitch2Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangSwitch3Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangCountdown1Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangCountdown2Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangCountdown3Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + default: - return Assets.logo; + return []; } } @@ -271,4 +386,23 @@ class AllDevicesModel { productName.hashCode ^ batteryLevel.hashCode; } + + Map devicesTypesMap = { + "AC": DeviceType.AC, + "GW": DeviceType.Gateway, + "CPS": DeviceType.CeilingSensor, + "DL": DeviceType.DoorLock, + "WPS": DeviceType.WallSensor, + "3G": DeviceType.ThreeGang, + "2G": DeviceType.TwoGang, + "1G": DeviceType.OneGang, + "CUR": DeviceType.Curtain, + "WH": DeviceType.WH, + "DS": DeviceType.DS, + "1GT": DeviceType.OneTouch, + "2GT": DeviceType.TowTouch, + "3GT": DeviceType.ThreeTouch, + "GD": DeviceType.GarageDoor, + "WL": DeviceType.WaterLeak, + }; } 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 8ed8c35e..06adfd6c 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 @@ -1,18 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_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/web_layout/web_scaffold.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'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { const DeviceManagementPage({super.key}); @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DeviceManagementBloc()..add(FetchDevices()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), + ), + BlocProvider( + create: (context) => DeviceManagementBloc()..add(FetchDevices()), + ), + ], child: WebScaffold( appBarTitle: FittedBox( child: Text( @@ -20,26 +33,86 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: Theme.of(context).textTheme.headlineLarge, ), ), + centerBody: BlocBuilder( + builder: (context, state) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: null, + ), + onPressed: () { + 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, + ), + ), + ), + TextButton( + style: TextButton.styleFrom( + backgroundColor: null, + ), + onPressed: () { + 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, + fontWeight: + (state is SelectedTabState) && state.selectedTab == true + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ), + ], + ); + }), rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocBuilder( - builder: (context, state) { - if (state is DeviceManagementLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is DeviceManagementLoaded || state is DeviceManagementFiltered) { - final devices = state is DeviceManagementLoaded - ? state.devices - : (state as DeviceManagementFiltered).filteredDevices; + scaffoldBody: BlocBuilder( + builder: (context, state) { + if (state is SelectedTabState && state.selectedTab) { + return const RoutinesView(); + } + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } - return DeviceManagementBody(devices: devices); - } else { - return const Center(child: Text('Error fetching Devices')); - } - }, - ), + return BlocBuilder( + builder: (context, deviceState) { + if (deviceState is DeviceManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (deviceState is DeviceManagementLoaded) { + return DeviceManagementBody(devices: deviceState.devices); + } else if (deviceState is DeviceManagementFiltered) { + return DeviceManagementBody( + devices: deviceState.filteredDevices); + } else { + return const Center(child: Text('Error fetching Devices')); + } + }, + ); + }), ), ); } } - - - 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 12c66403..0788e08d 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 @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/custom_table.dart'; import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_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/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; @@ -57,15 +57,12 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Low Battery ($lowBatteryCount)', ]; - final buttonLabel = - (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; return Column( children: [ Container( - padding: isLargeScreenSize(context) - ? const EdgeInsets.all(30) - : const EdgeInsets.all(15), + padding: isLargeScreenSize(context) ? const EdgeInsets.all(30) : const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -74,9 +71,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { tabs: tabs, selectedIndex: selectedIndex, onTabChanged: (index) { - context - .read() - .add(SelectedFilterChanged(index)); + context.read().add(SelectedFilterChanged(index)); }, ), const SizedBox(height: 20), @@ -98,14 +93,11 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { ), ); } else if (selectedDevices.length > 1) { - final productTypes = selectedDevices - .map((device) => device.productType) - .toSet(); + final productTypes = selectedDevices.map((device) => device.productType).toSet(); if (productTypes.length == 1) { showDialog( context: context, - builder: (context) => - DeviceBatchControlDialog( + builder: (context) => DeviceBatchControlDialog( devices: selectedDevices, ), ); @@ -119,9 +111,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: isControlButtonEnabled - ? Colors.white - : Colors.grey, + color: isControlButtonEnabled ? Colors.white : Colors.grey, ), ), ), @@ -132,17 +122,13 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { ), Expanded( child: Padding( - padding: isLargeScreenSize(context) - ? const EdgeInsets.all(30) - : const EdgeInsets.all(15), + padding: isLargeScreenSize(context) ? const EdgeInsets.all(30) : const EdgeInsets.all(15), child: DynamicTable( withSelectAll: true, cellDecoration: containerDecoration, onRowSelected: (index, isSelected, row) { final selectedDevice = devicesToShow[index]; - context - .read() - .add(SelectDevice(selectedDevice)); + context.read().add(SelectDevice(selectedDevice)); }, withCheckBox: true, size: MediaQuery.of(context).size, @@ -160,44 +146,27 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { ], data: devicesToShow.map((device) { final combinedSpaceNames = device.spaces != null - ? device.spaces! - .map((space) => space.spaceName) - .join(' > ') + - (device.community != null - ? ' > ${device.community!.name}' - : '') - : (device.community != null - ? device.community!.name - : ''); + ? device.spaces!.map((space) => space.spaceName).join(' > ') + + (device.community != null ? ' > ${device.community!.name}' : '') + : (device.community != null ? device.community!.name : ''); return [ device.name ?? '', device.productName ?? '', device.uuid ?? '', - (device.spaces != null && device.spaces!.isNotEmpty) - ? device.spaces![0].spaceName - : '', + (device.spaces != null && device.spaces!.isNotEmpty) ? device.spaces![0].spaceName : '', combinedSpaceNames, - device.batteryLevel != null - ? '${device.batteryLevel}%' - : '-', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.createTime ?? 0) * 1000)), + device.batteryLevel != null ? '${device.batteryLevel}%' : '-', + formatDateTime(DateTime.fromMillisecondsSinceEpoch((device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.updateTime ?? 0) * 1000)), + formatDateTime(DateTime.fromMillisecondsSinceEpoch((device.updateTime ?? 0) * 1000)), ]; }).toList(), onSelectionChanged: (selectedRows) { - context - .read() - .add(UpdateSelection(selectedRows)); + context.read().add(UpdateSelection(selectedRows)); }, - initialSelectedIds: context - .read() - .selectedDevices - .map((device) => device.uuid!) - .toList(), + initialSelectedIds: + context.read().selectedDevices.map((device) => device.uuid!).toList(), isEmpty: devicesToShow.isEmpty, ), ), 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 c68f9485..b9e36c25 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 @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class DeviceSearchFilters extends StatefulWidget { @@ -18,14 +18,6 @@ class _DeviceSearchFiltersState extends State final TextEditingController unitNameController = TextEditingController(); final TextEditingController productNameController = TextEditingController(); - @override - void dispose() { - communityController.dispose(); - unitNameController.dispose(); - productNameController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return isExtraLargeScreenSize(context) @@ -76,6 +68,7 @@ class _DeviceSearchFiltersState extends State community: communityController.text, searchField: true)); }, + onChanged: (p0) {}, ), ); } diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart new file mode 100644 index 00000000..f4db836b --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +class EffectPeriodBloc extends Bloc { + final daysMap = { + 'Sun': 'S', + 'Mon': 'M', + 'Tue': 'T', + 'Wed': 'W', + 'Thu': 'T', + 'Fri': 'F', + 'Sat': 'S', + }; + + EffectPeriodBloc() : super(EffectPeriodState.initial()) { + on(_onSetPeriod); + on(_onToggleDay); + on(_onSetCustomTime); + on(_onResetEffectivePeriod); + on(_onResetDays); + on(_setAllDays); + } + + void _onSetPeriod(SetPeriod event, Emitter emit) { + String startTime = ''; + String endTime = ''; + + switch (event.period) { + case EnumEffectivePeriodOptions.allDay: + startTime = '00:00'; + endTime = '23:59'; + break; + case EnumEffectivePeriodOptions.daytime: + startTime = '06:00'; + endTime = '18:00'; + break; + case EnumEffectivePeriodOptions.night: + startTime = '18:00'; + endTime = '06:00'; + break; + case EnumEffectivePeriodOptions.custom: + startTime = state.customStartTime ?? '00:00'; + endTime = state.customEndTime ?? '23:59'; + break; + default: + break; + } + + emit(state.copyWith( + selectedPeriod: event.period, customStartTime: startTime, customEndTime: endTime)); + } + + void _onToggleDay(ToggleDay event, Emitter emit) { + final daysList = state.selectedDaysBinary.split(''); + final dayIndex = getDayIndex(event.day); + if (daysList[dayIndex] == '1') { + daysList[dayIndex] = '0'; + } else { + daysList[dayIndex] = '1'; + } + final newDaysBinary = daysList.join(); + emit(state.copyWith(selectedDaysBinary: newDaysBinary)); + } + + void _onSetCustomTime(SetCustomTime event, Emitter emit) { + String startTime = event.startTime; + String endTime = event.endTime; + EnumEffectivePeriodOptions period; + + // Determine the period based on start and end times + if (startTime == '00:00' && endTime == '23:59') { + period = EnumEffectivePeriodOptions.allDay; + } else if (startTime == '06:00' && endTime == '18:00') { + period = EnumEffectivePeriodOptions.daytime; + } else if (startTime == '18:00' && endTime == '06:00') { + period = EnumEffectivePeriodOptions.night; + } else { + period = EnumEffectivePeriodOptions.custom; + } + + emit( + state.copyWith(customStartTime: startTime, customEndTime: endTime, selectedPeriod: period)); + } + + void _onResetEffectivePeriod(ResetEffectivePeriod event, Emitter emit) { + emit(state.copyWith( + selectedPeriod: EnumEffectivePeriodOptions.allDay, + customStartTime: '00:00', + customEndTime: '23:59', + selectedDaysBinary: '1111111')); + } + + void _onResetDays(ResetDays event, Emitter emit) { + emit(state.copyWith(selectedDaysBinary: '1111111')); + } + + int getDayIndex(String day) { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return days.indexOf(day); + } + + FutureOr _setAllDays(SetDays event, Emitter emit) { + emit(state.copyWith(selectedDaysBinary: event.daysBinary)); + } +} diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_event.dart b/lib/pages/routiens/bloc/effective_period/effect_period_event.dart new file mode 100644 index 00000000..e1a86915 --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_event.dart @@ -0,0 +1,50 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +abstract class EffectPeriodEvent extends Equatable { + const EffectPeriodEvent(); + + @override + List get props => []; +} + +class SetPeriod extends EffectPeriodEvent { + final EnumEffectivePeriodOptions period; + + const SetPeriod(this.period); + + @override + List get props => [period]; +} + +class ToggleDay extends EffectPeriodEvent { + final String day; + + const ToggleDay(this.day); + + @override + List get props => [day]; +} + +class SetCustomTime extends EffectPeriodEvent { + final String startTime; + final String endTime; + + const SetCustomTime(this.startTime, this.endTime); + + @override + List get props => [startTime, endTime]; +} + +class ResetEffectivePeriod extends EffectPeriodEvent {} + +class ResetDays extends EffectPeriodEvent { + @override + List get props => []; +} + +class SetDays extends EffectPeriodEvent { + final String daysBinary; + + const SetDays(this.daysBinary); +} diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_state.dart b/lib/pages/routiens/bloc/effective_period/effect_period_state.dart new file mode 100644 index 00000000..2f8b66c8 --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_state.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +class EffectPeriodState extends Equatable { + final EnumEffectivePeriodOptions selectedPeriod; + final String selectedDaysBinary; + final String? customStartTime; + final String? customEndTime; + + const EffectPeriodState({ + required this.selectedPeriod, + required this.selectedDaysBinary, + this.customStartTime, + this.customEndTime, + }); + + factory EffectPeriodState.initial() { + return const EffectPeriodState( + selectedPeriod: EnumEffectivePeriodOptions.allDay, + selectedDaysBinary: "1111111", // All days selected + customStartTime: "00:00", + customEndTime: "23:59", + ); + } + + EffectPeriodState copyWith({ + EnumEffectivePeriodOptions? selectedPeriod, + String? selectedDaysBinary, + String? customStartTime, + String? customEndTime, + }) { + return EffectPeriodState( + selectedPeriod: selectedPeriod ?? this.selectedPeriod, + selectedDaysBinary: selectedDaysBinary ?? this.selectedDaysBinary, + customStartTime: customStartTime ?? this.customStartTime, + customEndTime: customEndTime ?? this.customEndTime, + ); + } + + EnumEffectivePeriodOptions getEffectivePeriod() { + if (customStartTime == '00:00' && customEndTime == '23:59') { + return EnumEffectivePeriodOptions.allDay; + } else if (customStartTime == '06:00' && customEndTime == '18:00') { + return EnumEffectivePeriodOptions.daytime; + } else if (customStartTime == '18:00' && customEndTime == '06:00') { + return EnumEffectivePeriodOptions.night; + } else { + return EnumEffectivePeriodOptions.custom; + } + } + + @override + List get props => [selectedPeriod, selectedDaysBinary, customStartTime, customEndTime]; +} diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart new file mode 100644 index 00000000..760d5697 --- /dev/null +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; + +part 'functions_bloc_event.dart'; +part 'functions_bloc_state.dart'; + +class FunctionBloc extends Bloc { + FunctionBloc() : super(const FunctionBlocState()) { + on(_onInitializeFunctions); + on(_onAddFunction); + on(_onSelectFunction); + } + void _onAddFunction(AddFunction event, Emitter emit) { + final functions = List.from(state.addedFunctions); + final existingIndex = functions.indexWhere( + (f) => f.functionCode == event.functionData.functionCode, + ); + + if (existingIndex != -1) { + final existingData = functions[existingIndex]; + functions[existingIndex] = DeviceFunctionData( + entityId: event.functionData.entityId, + functionCode: event.functionData.functionCode, + operationName: event.functionData.operationName, + value: event.functionData.value ?? existingData.value, + valueDescription: event.functionData.valueDescription ?? + existingData.valueDescription, + condition: event.functionData.condition ?? existingData.condition, + ); + } else { + functions.add(event.functionData); + } + + emit(state.copyWith( + addedFunctions: functions, + selectedFunction: event.functionData.functionCode, + )); + } + + void _onInitializeFunctions( + InitializeFunctions event, + Emitter emit, + ) { + emit(state.copyWith(addedFunctions: event.functions)); + } + + DeviceFunctionData? getFunction(String functionCode) { + return state.addedFunctions.firstWhere( + (data) => data.functionCode == functionCode, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: functionCode, + operationName: '', + value: null, + ), + ); + } + + FutureOr _onSelectFunction( + SelectFunction event, Emitter emit) { + emit(state.copyWith( + selectedFunction: event.functionCode, + selectedOperationName: event.operationName)); + } +} diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart new file mode 100644 index 00000000..85ba62f4 --- /dev/null +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart @@ -0,0 +1,41 @@ +part of 'functions_bloc_bloc.dart'; + +abstract class FunctionBlocEvent extends Equatable { + const FunctionBlocEvent(); + + @override + List get props => []; +} + +class AddFunction extends FunctionBlocEvent { + final DeviceFunctionData functionData; + + const AddFunction({ + required this.functionData, + }); + + @override + List get props => [functionData]; +} + +class SelectFunction extends FunctionBlocEvent { + final String functionCode; + final String operationName; + + const SelectFunction({ + required this.functionCode, + required this.operationName, + }); + + @override + List get props => [functionCode, operationName]; +} + +class InitializeFunctions extends FunctionBlocEvent { + final List functions; + + const InitializeFunctions(this.functions); + + @override + List get props => [functions]; +} diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart new file mode 100644 index 00000000..2c6cd941 --- /dev/null +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart @@ -0,0 +1,29 @@ +part of 'functions_bloc_bloc.dart'; + +class FunctionBlocState extends Equatable { + final List addedFunctions; + final String? selectedFunction; + final String? selectedOperationName; + const FunctionBlocState({ + this.addedFunctions = const [], + this.selectedFunction, + this.selectedOperationName, + }); + + FunctionBlocState copyWith({ + List? addedFunctions, + String? selectedFunction, + String? selectedOperationName, + }) { + return FunctionBlocState( + addedFunctions: addedFunctions ?? this.addedFunctions, + selectedFunction: selectedFunction ?? this.selectedFunction, + selectedOperationName: + selectedOperationName ?? this.selectedOperationName, + ); + } + + @override + List get props => + [addedFunctions, selectedFunction, selectedOperationName]; +} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart new file mode 100644 index 00000000..841027b3 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -0,0 +1,595 @@ +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_and_autoamtion/create_automation_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/routine_details_model.dart'; +import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; +import 'package:syncrow_web/services/routines_api.dart'; + +part 'routine_event.dart'; +part 'routine_state.dart'; + +const spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6'; +const communityId = 'aff21a57-2f91-4e5c-b99b-0182c3ab65a9'; + +class RoutineBloc extends Bloc { + RoutineBloc() : super(const RoutineState()) { + on(_onAddToIfContainer); + on(_onAddToThenContainer); + on(_onLoadScenes); + on(_onLoadAutomation); + on(_onAddFunctionsToRoutine); + on(_onSearchRoutines); + on(_onAddSelectedIcon); + on(_onCreateScene); + on(_onRemoveDragCard); + on(_changeOperatorOperator); + on(_onEffectiveTimeEvent); + on(_onCreateAutomation); + on(_onSetRoutineName); + on(_onResetRoutineState); + on(_onGetSceneDetails); + on(_onGetAutomationDetails); + // on(_onInitializeRoutineState); + on(_deleteScene); + on(_deleteAutomation); + // on(_onRemoveFunction); + // on(_onClearFunctions); + } + + void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { + final updatedIfItems = List>.from(state.ifItems); + + // Find the index of the item in teh current itemsList + int index = updatedIfItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + // Replace the map if the index is valid + if (index != -1) { + updatedIfItems[index] = event.item; + } else { + updatedIfItems.add(event.item); + } + + if (event.isTabToRun) { + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); + } else { + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); + } + } + + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { + final currentItems = List>.from(state.thenItems); + + // Find the index of the item in teh current itemsList + int index = currentItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + // Replace the map if the index is valid + if (index != -1) { + currentItems[index] = event.item; + } else { + currentItems.add(event.item); + } + + emit(state.copyWith(thenItems: currentItems)); + } + + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { + try { + if (event.functions.isEmpty) return; + + List selectedFunction = + List.from(event.functions); + + Map> currentSelectedFunctions = + Map>.from(state.selectedFunctions); + if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { + List currentFunctions = + List.from( + currentSelectedFunctions[event.uniqueCustomId] ?? []); + + List functionCode = []; + for (int i = 0; i < selectedFunction.length; i++) { + for (int j = 0; j < currentFunctions.length; j++) { + if (selectedFunction[i].functionCode == + currentFunctions[j].functionCode) { + currentFunctions[j] = selectedFunction[i]; + if (!functionCode.contains(currentFunctions[j].functionCode)) { + functionCode.add(currentFunctions[j].functionCode); + } + } + } + } + + for (int i = 0; i < functionCode.length; i++) { + selectedFunction + .removeWhere((code) => code.functionCode == functionCode[i]); + } + + currentSelectedFunctions[event.uniqueCustomId] = + List.from(currentFunctions)..addAll(selectedFunction); + } else { + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); + } + + emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); + } catch (e) { + debugPrint('Error adding functions: $e'); + } + } + + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + + try { + final scenes = + await SceneApi.getScenesByUnitId(event.unitId, event.communityId); + emit(state.copyWith( + scenes: scenes, + isLoading: false, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', + )); + } + } + + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + + try { + final automations = await SceneApi.getAutomationByUnitId(event.unitId); + if (automations.isNotEmpty) { + emit(state.copyWith( + automations: automations, + isLoading: false, + )); + } else { + emit(state.copyWith( + isLoading: false, + loadAutomationErrorMessage: 'Failed to load automations', + errorMessage: '', + loadScenesErrorMessage: '', + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadAutomationErrorMessage: 'Failed to load automations', + errorMessage: '', + loadScenesErrorMessage: '', + )); + } + } + + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + await Future.delayed(const Duration(seconds: 1)); + emit(state.copyWith(isLoading: false, errorMessage: null)); + emit(state.copyWith(searchText: 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.expand((item) { + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + return functions.map((function) { + if (function.functionCode == 'automation') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: function.value, + executorProperty: null, + ); + } + + if (item['deviceId'] == 'delay') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'delay', + executorProperty: CreateSceneExecutorProperty( + functionCode: '', + functionValue: '', + delaySeconds: int.tryParse(function.value.toString()) ?? 0, + ), + ); + } + + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + executorProperty: CreateSceneExecutorProperty( + functionCode: function.functionCode, + functionValue: function.value, + delaySeconds: 0, + ), + ); + }); + }).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(_resetState()); + add(const LoadScenes(spaceId, communityId)); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: result['message'], + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Something went wrong', + )); + } + } + + Future _onCreateAutomation( + CreateAutomationEvent event, Emitter emit) async { + try { + if (state.routineName == null || state.routineName!.isEmpty) { + emit(state.copyWith( + errorMessage: 'Automation name is required', + )); + return; + } + + emit(state.copyWith(isLoading: true)); + + final conditions = state.ifItems.expand((item) { + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + return functions.map((function) { + return Condition( + code: state.selectedFunctions[item['uniqueCustomId']]!.indexOf( + function, + ) + + 1, + entityId: function.entityId, + entityType: 'device_report', + expr: ConditionExpr( + statusCode: function.functionCode, + comparator: function.condition ?? '==', + statusValue: function.value, + ), + ); + }); + }).toList(); + + if (conditions.isEmpty) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'At least one condition is required', + )); + return; + } + + final actions = state.thenItems.expand((item) { + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + return functions.map((function) { + if (function.functionCode == 'automation') { + return AutomationAction( + entityId: function.entityId, + actionExecutor: function.value, + ); + } + + if (item['deviceId'] == 'delay') { + return AutomationAction( + entityId: function.entityId, + actionExecutor: 'delay', + executorProperty: ExecutorProperty( + delaySeconds: int.tryParse(function.value.toString()) ?? 0, + ), + ); + } + + return AutomationAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + executorProperty: ExecutorProperty( + functionCode: function.functionCode, + functionValue: function.value, + ), + ); + }); + }).toList(); + + final createAutomationModel = CreateAutomationModel( + spaceUuid: spaceId, + automationName: state.routineName!, + decisionExpr: state.selectedAutomationOperator, + effectiveTime: EffectiveTime( + start: state.effectiveTime?.start ?? '00:00', + end: state.effectiveTime?.end ?? '23:59', + loops: state.effectiveTime?.loops ?? '1111111', + ), + conditions: conditions, + actions: actions, + ); + + final result = await SceneApi.createAutomation(createAutomationModel); + if (result['success']) { + emit(_resetState()); + add(const LoadAutomation(spaceId)); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: result['message'], + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Something went wrong', + )); + } + } + + FutureOr _onRemoveDragCard( + RemoveDragCard event, Emitter emit) { + if (event.isFromThen) { + final thenItems = List>.from(state.thenItems); + final selectedFunctions = + Map>.from(state.selectedFunctions); + + thenItems.removeAt(event.index); + selectedFunctions.remove(event.key); + emit(state.copyWith( + thenItems: thenItems, selectedFunctions: selectedFunctions)); + } else { + final ifItems = List>.from(state.ifItems); + final selectedFunctions = + Map>.from(state.selectedFunctions); + + ifItems.removeAt(event.index); + selectedFunctions.remove(event.key); + if (ifItems.isEmpty && state.thenItems.isEmpty) { + emit(state.copyWith( + ifItems: ifItems, + selectedFunctions: selectedFunctions, + isAutomation: false, + isTabToRun: false)); + } else { + emit(state.copyWith( + ifItems: ifItems, selectedFunctions: selectedFunctions)); + } + } + } + + FutureOr _changeOperatorOperator( + ChangeAutomationOperator event, Emitter emit) { + emit(state.copyWith( + selectedAutomationOperator: event.operator, + )); + } + + FutureOr _onEffectiveTimeEvent( + EffectiveTimePeriodEvent event, Emitter emit) { + emit(state.copyWith(effectiveTime: event.effectiveTime)); + } + + FutureOr _onSetRoutineName( + SetRoutineName event, Emitter emit) { + emit(state.copyWith(routineName: event.name)); + } + + Future _onGetSceneDetails( + GetSceneDetails event, Emitter emit) async { + try { + emit(state.copyWith( + isLoading: true, + isTabToRun: event.isTabToRun, + isUpdate: true, + sceneId: event.sceneId, + isAutomation: false)); + final sceneDetails = await SceneApi.getSceneDetails(event.sceneId); + add(InitializeRoutineState(sceneDetails)); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load scene details', + )); + } + } + + Future _onGetAutomationDetails( + GetAutomationDetails event, Emitter emit) async { + try { + emit(state.copyWith( + isLoading: true, + isAutomation: event.isAutomation, + automationId: event.automationId, + isTabToRun: false, + isUpdate: true, + )); + final automationDetails = + await SceneApi.getAutomationDetails(event.automationId); + add(InitializeRoutineState(automationDetails)); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load automation details', + )); + } + } + + // void _onInitializeRoutineState( + // InitializeRoutineState event, Emitter emit) { + // final routineDetails = event.routineDetails; + + // // Convert actions to draggable cards for the THEN container + // final thenItems = routineDetails.actions.map((action) { + // final Map cardData = { + // 'entityId': action.entityId, + // 'uniqueCustomId': const Uuid().v4(), + // 'deviceId': + // action.actionExecutor == 'delay' ? 'delay' : action.entityId, + // 'title': action.actionExecutor == 'delay' ? 'Delay' : 'Device', + // // fix this + // 'imagePath': + // action.actionExecutor == 'delay' ? Assets.delay : Assets.logo, + // }; + + // // Add functions to selectedFunctions + // if (action.executorProperty != null) { + // final functions = [ + // DeviceFunctionData( + // entityId: action.entityId, + // functionCode: action.executorProperty!.functionCode ?? '', + // value: action.executorProperty!.functionValue, + + // /// fix this + // operationName: action.executorProperty?.functionCode ?? ''), + // ]; + // state.selectedFunctions[cardData['uniqueCustomId']] = functions; + // } + + // return cardData; + // }).toList(); + + // // Convert conditions to draggable cards for the IF container + // final ifItems = routineDetails.conditions?.map((condition) { + // final Map cardData = { + // 'entityId': condition.entityId, + // 'uniqueCustomId': const Uuid().v4(), + // 'deviceId': condition.entityId, + + // /// fix this + // 'title': 'Device', + + // /// fix this + // 'imagePath': Assets.logo, + // }; + + // // Add functions to selectedFunctions + // final functions = [ + // DeviceFunctionData( + // entityId: condition.entityId, + // functionCode: condition.expr.statusCode, + // value: condition.expr.statusValue, + // condition: condition.expr.comparator, + // operationName: condition.expr.comparator, + // ), + // ]; + // state.selectedFunctions[cardData['uniqueCustomId']] = functions; + + // return cardData; + // }).toList() ?? + // []; + + // emit(state.copyWith( + // isLoading: false, + // routineName: routineDetails.name, + // selectedIcon: routineDetails.iconId, + // selectedAutomationOperator: routineDetails.decisionExpr, + // effectiveTime: routineDetails.effectiveTime, + // isAutomation: routineDetails.conditions != null, + // isTabToRun: routineDetails.conditions == null, + // thenItems: thenItems, + // ifItems: ifItems, + // selectedFunctions: Map.from(state.selectedFunctions), + // )); + // } + + RoutineState _resetState() { + return const RoutineState( + ifItems: [], + thenItems: [], + selectedFunctions: {}, + scenes: [], + automations: [], + isLoading: false, + errorMessage: null, + loadScenesErrorMessage: null, + loadAutomationErrorMessage: null, + searchText: '', + selectedIcon: null, + isTabToRun: false, + isAutomation: false, + selectedAutomationOperator: 'or', + effectiveTime: null, + routineName: null, + ); + } + + FutureOr _onResetRoutineState( + ResetRoutineState event, Emitter emit) { + emit(_resetState()); + } + + FutureOr _deleteScene(DeleteScene event, Emitter emit) { + try { + // emit(state.copyWith(isLoading: true)); + SceneApi.deleteScene(unitUuid: spaceId, sceneId: event.sceneId); + add(const LoadScenes(spaceId, communityId)); + //emit(_resetState()); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to delete scene', + )); + } + } + + FutureOr _deleteAutomation( + DeleteAutomation event, Emitter emit) { + try { + //emit(state.copyWith(isLoading: true)); + SceneApi.deleteAutomation( + unitUuid: spaceId, automationId: event.automationId); + add(const LoadAutomation(spaceId)); + // emit(_resetState()); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to delete automation', + )); + } + } +} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart new file mode 100644 index 00000000..82e7875b --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -0,0 +1,179 @@ +part of 'routine_bloc.dart'; + +abstract class RoutineEvent extends Equatable { + const RoutineEvent(); + + @override + List get props => []; +} + +class AddToIfContainer extends RoutineEvent { + final Map item; + final bool isTabToRun; + + const AddToIfContainer(this.item, this.isTabToRun); + + @override + List get props => [item, isTabToRun]; +} + +class AddToThenContainer extends RoutineEvent { + final Map item; + + const AddToThenContainer(this.item); + + @override + List get props => [item]; +} + +class LoadScenes extends RoutineEvent { + final String unitId; + final String communityId; + + const LoadScenes(this.unitId, this.communityId); + + @override + List get props => [unitId, communityId]; +} + +class LoadAutomation extends RoutineEvent { + final String unitId; + + const LoadAutomation(this.unitId); + + @override + List get props => [unitId]; +} + +class AddFunctionToRoutine extends RoutineEvent { + final List functions; + final String uniqueCustomId; + const AddFunctionToRoutine(this.functions, this.uniqueCustomId); + @override + List get props => [functions, uniqueCustomId]; +} + +class RemoveFunction extends RoutineEvent { + final DeviceFunctionData function; + const RemoveFunction(this.function); + @override + 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 RemoveDragCard extends RoutineEvent { + final int index; + final bool isFromThen; + final String key; + const RemoveDragCard( + {required this.index, required this.isFromThen, required this.key}); + @override + List get props => [index, isFromThen, key]; +} + +class ChangeAutomationOperator extends RoutineEvent { + final String operator; + const ChangeAutomationOperator({required this.operator}); + @override + List get props => [operator]; +} + +class EffectiveTimePeriodEvent extends RoutineEvent { + final EffectiveTime effectiveTime; + const EffectiveTimePeriodEvent(this.effectiveTime); + @override + List get props => [effectiveTime]; +} + +class CreateAutomationEvent extends RoutineEvent { + // final CreateAutomationModel createAutomationModel; + final String? automationId; + final bool updateAutomation; + + const CreateAutomationEvent({ + //required this.createAutomationModel, + this.automationId, + this.updateAutomation = false, + }); + @override + List get props => []; +} + +class SetRoutineName extends RoutineEvent { + final String name; + const SetRoutineName(this.name); + @override + List get props => [name]; +} + +class GetSceneDetails extends RoutineEvent { + final String sceneId; + final bool isUpdate; + final bool isTabToRun; + const GetSceneDetails({ + required this.sceneId, + required this.isUpdate, + required this.isTabToRun, + }); + @override + List get props => [sceneId]; +} + +class GetAutomationDetails extends RoutineEvent { + final String automationId; + final bool isUpdate; + final bool isAutomation; + const GetAutomationDetails({ + required this.automationId, + this.isUpdate = false, + this.isAutomation = false, + }); + @override + List get props => [automationId]; +} + +class InitializeRoutineState extends RoutineEvent { + final RoutineDetailsModel routineDetails; + const InitializeRoutineState(this.routineDetails); + @override + List get props => [routineDetails]; +} + +class DeleteScene extends RoutineEvent { + final String sceneId; + final String unitUuid; + const DeleteScene({required this.sceneId, required this.unitUuid}); + @override + List get props => [sceneId]; +} + +class DeleteAutomation extends RoutineEvent { + final String automationId; + final String unitUuid; + const DeleteAutomation({required this.automationId, required this.unitUuid}); + @override + List get props => [automationId]; +} + +class ResetRoutineState extends RoutineEvent {} + +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 new file mode 100644 index 00000000..8f8d9b5c --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -0,0 +1,117 @@ +part of 'routine_bloc.dart'; + +class RoutineState extends Equatable { + final List> ifItems; + final List> thenItems; + final List> availableCards; + final List scenes; + final List automations; + final Map> selectedFunctions; + final bool isLoading; + final String? errorMessage; + final String? loadScenesErrorMessage; + final String? loadAutomationErrorMessage; + final String? routineName; + final String? selectedIcon; + final String? searchText; + final bool isTabToRun; + final bool isAutomation; + final String selectedAutomationOperator; + final EffectiveTime? effectiveTime; + final String? sceneId; + final String? automationId; + final bool? isUpdate; + + const RoutineState({ + this.ifItems = const [], + this.thenItems = const [], + this.availableCards = const [], + this.scenes = const [], + this.automations = const [], + this.selectedFunctions = const {}, + this.isLoading = false, + this.errorMessage, + this.routineName, + this.selectedIcon, + this.loadScenesErrorMessage, + this.loadAutomationErrorMessage, + this.searchText, + this.isTabToRun = false, + this.isAutomation = false, + this.selectedAutomationOperator = 'or', + this.effectiveTime, + this.sceneId, + this.automationId, + this.isUpdate, + }); + + RoutineState copyWith({ + List>? ifItems, + List>? thenItems, + List? scenes, + List? automations, + Map>? selectedFunctions, + bool? isLoading, + String? errorMessage, + String? routineName, + String? selectedIcon, + String? loadAutomationErrorMessage, + String? loadScenesErrorMessage, + String? searchText, + bool? isTabToRun, + bool? isAutomation, + String? selectedAutomationOperator, + EffectiveTime? effectiveTime, + String? sceneId, + String? automationId, + bool? isUpdate, + }) { + return RoutineState( + ifItems: ifItems ?? this.ifItems, + thenItems: thenItems ?? this.thenItems, + scenes: scenes ?? this.scenes, + automations: automations ?? this.automations, + selectedFunctions: selectedFunctions ?? this.selectedFunctions, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + routineName: routineName ?? this.routineName, + selectedIcon: selectedIcon ?? this.selectedIcon, + loadScenesErrorMessage: + loadScenesErrorMessage ?? this.loadScenesErrorMessage, + loadAutomationErrorMessage: + loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, + searchText: searchText ?? this.searchText, + isTabToRun: isTabToRun ?? this.isTabToRun, + isAutomation: isAutomation ?? this.isAutomation, + selectedAutomationOperator: + selectedAutomationOperator ?? this.selectedAutomationOperator, + effectiveTime: effectiveTime ?? this.effectiveTime, + sceneId: sceneId ?? this.sceneId, + automationId: automationId ?? this.automationId, + isUpdate: isUpdate ?? this.isUpdate, + ); + } + + @override + List get props => [ + ifItems, + thenItems, + scenes, + automations, + selectedFunctions, + isLoading, + errorMessage, + routineName, + selectedIcon, + loadScenesErrorMessage, + loadAutomationErrorMessage, + searchText, + isTabToRun, + isAutomation, + selectedAutomationOperator, + effectiveTime, + sceneId, + automationId, + isUpdate + ]; +} diff --git a/lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart b/lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart new file mode 100644 index 00000000..f70aed34 --- /dev/null +++ b/lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart @@ -0,0 +1,53 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_state.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; +import 'package:syncrow_web/services/routines_api.dart'; + +class SettingBloc extends Bloc { + bool isExpanded = false; + String selectedIcon = ''; + List iconModelList = []; + + SettingBloc() : super(const InitialState()) { + on(_initialSetting); + on(_fetchIcons); + on(_selectIcon); + } + + void _initialSetting(InitialEvent event, Emitter emit) async { + try { + emit(const LoadingState()); + selectedIcon = event.selectedIcon; + emit(TabToRunSettingLoaded( + showInDevice: true, selectedIcon: event.selectedIcon, iconList: iconModelList)); + } catch (e) { + emit(const FailedState(error: 'Something went wrong')); + } + } + + void _fetchIcons(FetchIcons event, Emitter emit) async { + try { + isExpanded = event.expanded; + emit(const LoadingState()); + if (isExpanded) { + iconModelList = await SceneApi.getIcon(); + emit(TabToRunSettingLoaded( + showInDevice: true, selectedIcon: selectedIcon, iconList: iconModelList)); + } + } catch (e) { + emit(const FailedState(error: 'Something went wrong')); + } + } + + void _selectIcon(SelectIcon event, Emitter emit) async { + try { + emit(const LoadingState()); + selectedIcon = event.iconId; + emit(TabToRunSettingLoaded( + showInDevice: true, selectedIcon: event.iconId, iconList: iconModelList)); + } catch (e) { + emit(const FailedState(error: 'Something went wrong')); + } + } +} diff --git a/lib/pages/routiens/bloc/setting_bloc/setting_event.dart b/lib/pages/routiens/bloc/setting_bloc/setting_event.dart new file mode 100644 index 00000000..0580e51e --- /dev/null +++ b/lib/pages/routiens/bloc/setting_bloc/setting_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +abstract class SettingEvent extends Equatable { + const SettingEvent(); + + @override + List get props => []; +} + +class InitialEvent extends SettingEvent { + final String selectedIcon; + const InitialEvent({required this.selectedIcon}); + + @override + List get props => [selectedIcon]; +} + +class FetchIcons extends SettingEvent { + final bool expanded; + const FetchIcons({required this.expanded}); + + @override + List get props => [expanded]; +} + +class SelectIcon extends SettingEvent { + final String iconId; + const SelectIcon({required this.iconId}); + + @override + List get props => [iconId]; +} diff --git a/lib/pages/routiens/bloc/setting_bloc/setting_state.dart b/lib/pages/routiens/bloc/setting_bloc/setting_state.dart new file mode 100644 index 00000000..7c88d67c --- /dev/null +++ b/lib/pages/routiens/bloc/setting_bloc/setting_state.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; + +abstract class SettingState extends Equatable { + const SettingState(); + + @override + List get props => []; +} + +class LoadingState extends SettingState { + const LoadingState(); + + @override + List get props => []; +} + +class InitialState extends SettingState { + const InitialState(); + + @override + List get props => []; +} + +class IconLoadedState extends SettingState { + final List status; + + const IconLoadedState(this.status); + + @override + List get props => [status]; +} + +class TabToRunSettingLoaded extends SettingState { + final String selectedIcon; + final List iconList; + final bool showInDevice; + + const TabToRunSettingLoaded({ + required this.selectedIcon, + required this.iconList, + required this.showInDevice, + }); + + @override + List get props => [selectedIcon, iconList, showInDevice]; +} + +class FailedState extends SettingState { + final String error; + + const FailedState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart new file mode 100644 index 00000000..1dd84c19 --- /dev/null +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/ac_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; + +class DeviceDialogHelper { + static Future?> showDeviceDialog( + BuildContext context, + Map data, { + required bool removeComparetors, + }) async { + final functions = data['functions'] as List; + + try { + final result = await _getDialogForDeviceType( + context, + data['productType'], + data, + functions, + removeComparetors: removeComparetors, + ); + + if (result != null) { + return result; + } + } catch (e) { + debugPrint('Error: $e'); + } + + return null; + } + + static Future?> _getDialogForDeviceType( + BuildContext context, + String productType, + Map data, + List functions, + {required bool removeComparetors}) async { + final routineBloc = context.read(); + final deviceSelectedFunctions = + routineBloc.state.selectedFunctions[data['uniqueCustomId']] ?? []; + + switch (productType) { + case 'AC': + return ACHelper.showACFunctionsDialog( + context, + functions, + data['device'], + deviceSelectedFunctions, + data['uniqueCustomId'], + removeComparetors); + + case '1G': + return OneGangSwitchHelper.showSwitchFunctionsDialog( + context, + functions, + data['device'], + deviceSelectedFunctions, + data['uniqueCustomId'], + removeComparetors); + case '2G': + return TwoGangSwitchHelper.showSwitchFunctionsDialog( + context, + functions, + data['device'], + deviceSelectedFunctions, + data['uniqueCustomId'], + removeComparetors); + case '3G': + return ThreeGangSwitchHelper.showSwitchFunctionsDialog( + context, + functions, + data['device'], + deviceSelectedFunctions, + data['uniqueCustomId'], + removeComparetors); + default: + return null; + } + } +} diff --git a/lib/pages/routiens/helper/duration_format_helper.dart b/lib/pages/routiens/helper/duration_format_helper.dart new file mode 100644 index 00000000..1f22c9b2 --- /dev/null +++ b/lib/pages/routiens/helper/duration_format_helper.dart @@ -0,0 +1,15 @@ +class DurationFormatMixin { + static String formatDuration(int seconds) { + if (seconds >= 3600) { + final hours = (seconds / 3600).floor(); + final remainingMinutes = ((seconds % 3600) / 60).floor(); + final remainingSeconds = seconds % 60; + return '$hours h ${remainingMinutes}m ${remainingSeconds}s'; + } else if (seconds >= 60) { + final minutes = (seconds / 60).floor(); + final remainingSeconds = seconds % 60; + return '$minutes m ${remainingSeconds}s'; + } + return '${seconds}s'; + } +} 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..df9e8323 --- /dev/null +++ b/lib/pages/routiens/helper/save_routine_helper.dart @@ -0,0 +1,157 @@ +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 (state.isTabToRun) + ListTile( + leading: SvgPicture.asset( + Assets.tabToRun, + width: 24, + height: 24, + ), + title: const Text('Tab to run'), + ), + if (state.isAutomation) + ...state.ifItems.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(), + ), + ); + }), + ], + ), + ), + 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, false), + onConfirm: () { + if (state.isAutomation) { + context.read().add(const CreateAutomationEvent()); + } else { + context.read().add(const CreateSceneEvent()); + } + + Navigator.pop(context, true); + }, + isConfirmEnabled: true, + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/models/ac/ac_function.dart b/lib/pages/routiens/models/ac/ac_function.dart new file mode 100644 index 00000000..8feccda7 --- /dev/null +++ b/lib/pages/routiens/models/ac/ac_function.dart @@ -0,0 +1,153 @@ +import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class ACFunction extends DeviceFunction { + ACFunction({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + }); + + List getOperationalValues(); +} + +class SwitchFunction extends ACFunction { + SwitchFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'switch', + operationName: 'Power', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + ACOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ModeFunction extends ACFunction { + ModeFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'mode', + operationName: 'Mode', + icon: Assets.assetsFreezing, + ); + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsAcCooling, + description: "Cooling", + value: TempModes.cold.name, + ), + ACOperationalValue( + icon: Assets.assetsAcHeating, + description: "Heating", + value: TempModes.hot.name, + ), + ACOperationalValue( + icon: Assets.assetsFanSpeed, + description: "Ventilation", + value: TempModes.wind.name, + ), + ]; +} + +class TempSetFunction extends ACFunction { + final int min; + final int max; + final int step; + + TempSetFunction({required super.deviceId, required super.deviceName}) + : min = 160, + max = 300, + step = 1, + super( + code: 'temp_set', + operationName: 'Set Temperature', + icon: Assets.assetsTempreture, + ); + + @override + List getOperationalValues() { + List values = []; + for (int temp = min; temp <= max; temp += step) { + values.add(ACOperationalValue( + icon: Assets.assetsTempreture, + description: "${temp / 10}°C", + value: temp, + )); + } + return values; + } +} + +class LevelFunction extends ACFunction { + LevelFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'level', + operationName: 'Fan Speed', + icon: Assets.assetsFanSpeed, + ); + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsAcFanLow, + description: "LOW", + value: FanSpeeds.low.name, + ), + ACOperationalValue( + icon: Assets.assetsAcFanMiddle, + description: "MIDDLE", + value: FanSpeeds.middle.name, + ), + ACOperationalValue( + icon: Assets.assetsAcFanHigh, + description: "HIGH", + value: FanSpeeds.high.name, + ), + ACOperationalValue( + icon: Assets.assetsAcFanAuto, + description: "AUTO", + value: FanSpeeds.auto.name, + ), + ]; +} + +class ChildLockFunction extends ACFunction { + ChildLockFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'child_lock', + operationName: 'Child Lock', + icon: Assets.assetsChildLock, + ); + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsSceneChildLock, + description: "Lock", + value: true, + ), + ACOperationalValue( + icon: Assets.assetsSceneChildUnlock, + description: "Unlock", + value: false, + ), + ]; +} diff --git a/lib/pages/routiens/models/ac/ac_operational_value.dart b/lib/pages/routiens/models/ac/ac_operational_value.dart new file mode 100644 index 00000000..4ca45d10 --- /dev/null +++ b/lib/pages/routiens/models/ac/ac_operational_value.dart @@ -0,0 +1,11 @@ +class ACOperationalValue { + final String icon; + final String description; + final dynamic value; + + ACOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart b/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart new file mode 100644 index 00000000..c02e5dab --- /dev/null +++ b/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart @@ -0,0 +1,194 @@ +import 'dart:convert'; + +class CreateAutomationModel { + String spaceUuid; + String automationName; + String decisionExpr; + EffectiveTime effectiveTime; + List conditions; + List actions; + + CreateAutomationModel({ + required this.spaceUuid, + required this.automationName, + required this.decisionExpr, + required this.effectiveTime, + required this.conditions, + required this.actions, + }); + + Map toMap() { + return { + 'spaceUuid': spaceUuid, + 'automationName': automationName, + 'decisionExpr': decisionExpr, + 'effectiveTime': effectiveTime.toMap(), + 'conditions': conditions.map((x) => x.toMap()).toList(), + 'actions': actions.map((x) => x.toMap()).toList(), + }; + } + + factory CreateAutomationModel.fromMap(Map map) { + return CreateAutomationModel( + spaceUuid: map['spaceUuid'] ?? '', + automationName: map['automationName'] ?? '', + decisionExpr: map['decisionExpr'] ?? '', + effectiveTime: EffectiveTime.fromMap(map['effectiveTime']), + conditions: List.from( + map['conditions']?.map((x) => Condition.fromMap(x)) ?? []), + actions: List.from( + map['actions']?.map((x) => AutomationAction.fromMap(x)) ?? []), + ); + } + + String toJson() => json.encode(toMap()); + + factory CreateAutomationModel.fromJson(String source) => + CreateAutomationModel.fromMap(json.decode(source)); +} + +class EffectiveTime { + String start; + String end; + String loops; + + EffectiveTime({ + required this.start, + required this.end, + required this.loops, + }); + + Map toMap() { + return { + 'start': start, + 'end': end, + 'loops': loops, + }; + } + + factory EffectiveTime.fromMap(Map map) { + return EffectiveTime( + start: map['start'] ?? '', + end: map['end'] ?? '', + loops: map['loops'] ?? '', + ); + } +} + +class Condition { + int code; + String entityId; + String entityType; + ConditionExpr expr; + + Condition({ + required this.code, + required this.entityId, + required this.entityType, + required this.expr, + }); + + Map toMap() { + return { + 'code': code, + 'entityId': entityId, + 'entityType': entityType, + 'expr': expr.toMap(), + }; + } + + factory Condition.fromMap(Map map) { + return Condition( + code: map['code']?.toInt() ?? 0, + entityId: map['entityId'] ?? '', + entityType: map['entityType'] ?? '', + expr: ConditionExpr.fromMap(map['expr']), + ); + } +} + +class ConditionExpr { + String statusCode; + String comparator; + dynamic statusValue; + + ConditionExpr({ + required this.statusCode, + required this.comparator, + required this.statusValue, + }); + + Map toMap() { + return { + 'statusCode': statusCode, + 'comparator': comparator, + 'statusValue': statusValue, + }; + } + + factory ConditionExpr.fromMap(Map map) { + return ConditionExpr( + statusCode: map['statusCode'] ?? '', + comparator: map['comparator'] ?? '', + statusValue: map['statusValue'], + ); + } +} + +class AutomationAction { + String entityId; + String actionExecutor; + ExecutorProperty? executorProperty; + + AutomationAction({ + required this.entityId, + required this.actionExecutor, + this.executorProperty, + }); + + Map toMap() { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'executorProperty': executorProperty?.toMap(), + }; + } + + factory AutomationAction.fromMap(Map map) { + return AutomationAction( + entityId: map['entityId'] ?? '', + actionExecutor: map['actionExecutor'] ?? '', + executorProperty: map['executorProperty'] != null + ? ExecutorProperty.fromMap(map['executorProperty']) + : null, + ); + } +} + +class ExecutorProperty { + String? functionCode; + dynamic functionValue; + int? delaySeconds; + + ExecutorProperty({ + this.functionCode, + this.functionValue, + this.delaySeconds, + }); + + Map toMap() { + return { + if (functionCode != null) 'functionCode': functionCode, + if (functionValue != null) 'functionValue': functionValue, + if (delaySeconds != null) 'delaySeconds': delaySeconds, + }; + } + + factory ExecutorProperty.fromMap(Map map) { + return ExecutorProperty( + functionCode: map['functionCode'], + functionValue: map['functionValue'], + delaySeconds: map['delaySeconds']?.toInt(), + ); + } +} diff --git a/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart b/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart new file mode 100644 index 00000000..c669aa9a --- /dev/null +++ b/lib/pages/routiens/models/create_scene_and_autoamtion/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/models/delay/delay_fucntions.dart b/lib/pages/routiens/models/delay/delay_fucntions.dart new file mode 100644 index 00000000..ff04251a --- /dev/null +++ b/lib/pages/routiens/models/delay/delay_fucntions.dart @@ -0,0 +1,28 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DelayFunction extends BaseSwitchFunction { + DelayFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'delay', + operationName: 'Delay', + icon: Assets.delay, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "Duration in seconds", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; + + int convertToSeconds(int hours, int minutes) { + return (hours * 3600) + (minutes * 60); + } +} diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart new file mode 100644 index 00000000..59b63a4f --- /dev/null +++ b/lib/pages/routiens/models/device_functions.dart @@ -0,0 +1,84 @@ +abstract class DeviceFunction { + final String deviceId; + final String deviceName; + final String code; + final String operationName; + final String icon; + + DeviceFunction({ + required this.deviceId, + required this.deviceName, + required this.code, + required this.operationName, + required this.icon, + }); +} + +class DeviceFunctionData { + final String entityId; + final String actionExecutor; + final String functionCode; + final String operationName; + final dynamic value; + final String? condition; + final String? valueDescription; + + DeviceFunctionData({ + required this.entityId, + this.actionExecutor = 'device_issue', + required this.functionCode, + required this.operationName, + required this.value, + this.condition, + this.valueDescription, + }); + + Map toJson() { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'function': functionCode, + 'operationName': operationName, + 'value': value, + if (condition != null) 'condition': condition, + if (valueDescription != null) 'valueDescription': valueDescription, + }; + } + + factory DeviceFunctionData.fromJson(Map json) { + return DeviceFunctionData( + entityId: json['entityId'], + actionExecutor: json['actionExecutor'] ?? 'function', + functionCode: json['function'], + operationName: json['operationName'], + value: json['value'], + condition: json['condition'], + valueDescription: json['valueDescription'], + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is DeviceFunctionData && + other.entityId == entityId && + other.actionExecutor == actionExecutor && + other.functionCode == functionCode && + other.operationName == operationName && + other.value == value && + other.condition == condition && + other.valueDescription == valueDescription; + } + + @override + int get hashCode { + return entityId.hashCode ^ + actionExecutor.hashCode ^ + functionCode.hashCode ^ + operationName.hashCode ^ + value.hashCode ^ + condition.hashCode ^ + valueDescription.hashCode; + } +} diff --git a/lib/pages/routiens/models/gang_switches/base_switch_function.dart b/lib/pages/routiens/models/gang_switches/base_switch_function.dart new file mode 100644 index 00000000..f180b203 --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/base_switch_function.dart @@ -0,0 +1,14 @@ +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; + +abstract class BaseSwitchFunction extends DeviceFunction { + BaseSwitchFunction({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + }); + + List getOperationalValues(); +} diff --git a/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart b/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart new file mode 100644 index 00000000..2e20e40e --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart @@ -0,0 +1,47 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class OneGangSwitchFunction extends BaseSwitchFunction { + OneGangSwitchFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_1', + operationName: 'Light Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class OneGangCountdownFunction extends BaseSwitchFunction { + OneGangCountdownFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_1', + operationName: 'Light Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} diff --git a/lib/pages/routiens/models/gang_switches/switch_operational_value.dart b/lib/pages/routiens/models/gang_switches/switch_operational_value.dart new file mode 100644 index 00000000..eabd4a35 --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/switch_operational_value.dart @@ -0,0 +1,17 @@ +class SwitchOperationalValue { + final String icon; + final String description; + final dynamic value; + final double? minValue; + final double? maxValue; + final double? stepValue; + + SwitchOperationalValue({ + required this.icon, + required this.value, + this.description = '', + this.minValue, + this.maxValue, + this.stepValue, + }); +} diff --git a/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart b/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart new file mode 100644 index 00000000..7f4710f0 --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart @@ -0,0 +1,138 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ThreeGangSwitch1Function extends BaseSwitchFunction { + ThreeGangSwitch1Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_1', + operationName: 'Light 1 Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ThreeGangCountdown1Function extends BaseSwitchFunction { + ThreeGangCountdown1Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_1', + operationName: 'Light 1 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} + +class ThreeGangSwitch2Function extends BaseSwitchFunction { + ThreeGangSwitch2Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_2', + operationName: 'Light 2 Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ThreeGangCountdown2Function extends BaseSwitchFunction { + ThreeGangCountdown2Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_2', + operationName: 'Light 2 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} + +class ThreeGangSwitch3Function extends BaseSwitchFunction { + ThreeGangSwitch3Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_3', + operationName: 'Light 3 Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ThreeGangCountdown3Function extends BaseSwitchFunction { + ThreeGangCountdown3Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_3', + operationName: 'Light 3 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} diff --git a/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart b/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart new file mode 100644 index 00000000..91bda15c --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart @@ -0,0 +1,93 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class TwoGangSwitch1Function extends BaseSwitchFunction { + TwoGangSwitch1Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_1', + operationName: 'Light 1 Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class TwoGangSwitch2Function extends BaseSwitchFunction { + TwoGangSwitch2Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_2', + operationName: 'Light 2 Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class TwoGangCountdown1Function extends BaseSwitchFunction { + TwoGangCountdown1Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_1', + operationName: 'Light 1 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} + +class TwoGangCountdown2Function extends BaseSwitchFunction { + TwoGangCountdown2Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_2', + operationName: 'Light 2 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} diff --git a/lib/pages/routiens/models/icon_model.dart b/lib/pages/routiens/models/icon_model.dart new file mode 100644 index 00000000..70f15e8c --- /dev/null +++ b/lib/pages/routiens/models/icon_model.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +class IconModel { + final String uuid; + final DateTime createdAt; + final DateTime updatedAt; + final String iconBase64; + + IconModel({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.iconBase64, + }); + + // Method to decode the icon from Base64 and return as Uint8List + Uint8List get iconBytes => base64Decode(iconBase64); + + // Factory constructor to create an instance from JSON + factory IconModel.fromJson(Map json) { + return IconModel( + uuid: json['uuid'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + iconBase64: json['icon'] as String, + ); + } + + // Method to convert an instance back to JSON + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'icon': iconBase64, + }; + } +} diff --git a/lib/pages/routiens/models/routine_details_model.dart b/lib/pages/routiens/models/routine_details_model.dart new file mode 100644 index 00000000..8ad48c58 --- /dev/null +++ b/lib/pages/routiens/models/routine_details_model.dart @@ -0,0 +1,262 @@ +import 'dart:convert'; + +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart'; + +class RoutineDetailsModel { + final String spaceUuid; + final String name; + final String decisionExpr; + final List actions; + final String? iconId; + final bool? showInDevice; + final EffectiveTime? effectiveTime; + final List? conditions; + final String? type; + + RoutineDetailsModel({ + required this.spaceUuid, + required this.name, + required this.decisionExpr, + required this.actions, + this.iconId, + this.showInDevice, + this.effectiveTime, + this.conditions, + this.type, + }); + + // Convert to CreateSceneModel + CreateSceneModel toCreateSceneModel() { + return CreateSceneModel( + spaceUuid: spaceUuid, + iconId: iconId ?? '', + showInDevice: showInDevice ?? false, + sceneName: name, + decisionExpr: decisionExpr, + actions: actions.map((a) => a.toCreateSceneAction()).toList(), + ); + } + + // Convert to CreateAutomationModel + CreateAutomationModel toCreateAutomationModel() { + return CreateAutomationModel( + spaceUuid: spaceUuid, + automationName: name, + decisionExpr: decisionExpr, + effectiveTime: + effectiveTime ?? EffectiveTime(start: '', end: '', loops: ''), + conditions: conditions?.map((c) => c.toCondition()).toList() ?? [], + actions: actions.map((a) => a.toAutomationAction()).toList(), + ); + } + + Map toMap() { + return { + 'spaceUuid': spaceUuid, + 'name': name, + 'decisionExpr': decisionExpr, + 'actions': actions.map((x) => x.toMap()).toList(), + if (iconId != null) 'iconId': iconId, + if (showInDevice != null) 'showInDevice': showInDevice, + if (effectiveTime != null) 'effectiveTime': effectiveTime!.toMap(), + if (conditions != null) + 'conditions': conditions!.map((x) => x.toMap()).toList(), + if (type != null) 'type': type, + }; + } + + factory RoutineDetailsModel.fromMap(Map map) { + return RoutineDetailsModel( + spaceUuid: map['spaceUuid'] ?? '', + name: map['name'] ?? '', + decisionExpr: map['decisionExpr'] ?? '', + actions: List.from( + map['actions']?.map((x) => RoutineAction.fromMap(x)) ?? [], + ), + iconId: map['iconId'], + showInDevice: map['showInDevice'], + effectiveTime: map['effectiveTime'] != null + ? EffectiveTime.fromMap(map['effectiveTime']) + : null, + conditions: map['conditions'] != null + ? List.from( + map['conditions'].map((x) => RoutineCondition.fromMap(x))) + : null, + type: map['type'], + ); + } + + String toJson() => json.encode(toMap()); + + factory RoutineDetailsModel.fromJson(String source) => + RoutineDetailsModel.fromMap(json.decode(source)); +} + +class RoutineAction { + final String entityId; + final String actionExecutor; + final RoutineExecutorProperty? executorProperty; + + RoutineAction({ + required this.entityId, + required this.actionExecutor, + this.executorProperty, + }); + + CreateSceneAction toCreateSceneAction() { + return CreateSceneAction( + entityId: entityId, + actionExecutor: actionExecutor, + executorProperty: executorProperty?.toCreateSceneExecutorProperty(), + ); + } + + AutomationAction toAutomationAction() { + return AutomationAction( + entityId: entityId, + actionExecutor: actionExecutor, + executorProperty: executorProperty?.toExecutorProperty(), + ); + } + + Map toMap() { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + if (executorProperty != null) + 'executorProperty': executorProperty!.toMap(), + }; + } + + factory RoutineAction.fromMap(Map map) { + return RoutineAction( + entityId: map['entityId'] ?? '', + actionExecutor: map['actionExecutor'] ?? '', + executorProperty: map['executorProperty'] != null + ? RoutineExecutorProperty.fromMap(map['executorProperty']) + : null, + ); + } +} + +class RoutineExecutorProperty { + final String? functionCode; + final dynamic functionValue; + final int? delaySeconds; + + RoutineExecutorProperty({ + this.functionCode, + this.functionValue, + this.delaySeconds, + }); + + CreateSceneExecutorProperty toCreateSceneExecutorProperty() { + return CreateSceneExecutorProperty( + functionCode: functionCode ?? '', + functionValue: functionValue, + delaySeconds: delaySeconds ?? 0, + ); + } + + ExecutorProperty toExecutorProperty() { + return ExecutorProperty( + functionCode: functionCode, + functionValue: functionValue, + delaySeconds: delaySeconds, + ); + } + + Map toMap() { + return { + if (functionCode != null) 'functionCode': functionCode, + if (functionValue != null) 'functionValue': functionValue, + if (delaySeconds != null) 'delaySeconds': delaySeconds, + }; + } + + factory RoutineExecutorProperty.fromMap(Map map) { + return RoutineExecutorProperty( + functionCode: map['functionCode'], + functionValue: map['functionValue'], + delaySeconds: map['delaySeconds']?.toInt(), + ); + } +} + +class RoutineCondition { + final int code; + final String entityId; + final String entityType; + final RoutineConditionExpr expr; + + RoutineCondition({ + required this.code, + required this.entityId, + required this.entityType, + required this.expr, + }); + + Condition toCondition() { + return Condition( + code: code, + entityId: entityId, + entityType: entityType, + expr: expr.toConditionExpr(), + ); + } + + Map toMap() { + return { + 'code': code, + 'entityId': entityId, + 'entityType': entityType, + 'expr': expr.toMap(), + }; + } + + factory RoutineCondition.fromMap(Map map) { + return RoutineCondition( + code: map['code']?.toInt() ?? 0, + entityId: map['entityId'] ?? '', + entityType: map['entityType'] ?? '', + expr: RoutineConditionExpr.fromMap(map['expr']), + ); + } +} + +class RoutineConditionExpr { + final String statusCode; + final String comparator; + final dynamic statusValue; + + RoutineConditionExpr({ + required this.statusCode, + required this.comparator, + required this.statusValue, + }); + + ConditionExpr toConditionExpr() { + return ConditionExpr( + statusCode: statusCode, + comparator: comparator, + statusValue: statusValue, + ); + } + + Map toMap() { + return { + 'statusCode': statusCode, + 'comparator': comparator, + 'statusValue': statusValue, + }; + } + + factory RoutineConditionExpr.fromMap(Map map) { + return RoutineConditionExpr( + statusCode: map['statusCode'] ?? '', + comparator: map['comparator'] ?? '', + statusValue: map['statusValue'], + ); + } +} diff --git a/lib/pages/routiens/models/routine_item.dart b/lib/pages/routiens/models/routine_item.dart new file mode 100644 index 00000000..08263d53 --- /dev/null +++ b/lib/pages/routiens/models/routine_item.dart @@ -0,0 +1,30 @@ +// import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; + +// class RoutineItem { +// final AllDevicesModel device; +// final String? function; +// final dynamic value; + +// RoutineItem({ +// required this.device, +// this.function, +// this.value, +// }); + +// Map toMap() { +// return { +// 'device': device, +// 'function': function, +// 'value': value, +// }; +// } + +// factory RoutineItem.fromMap(Map map) { +// return RoutineItem( +// device: map['device'] as AllDevicesModel, +// function: map['function'], +// value: map['value'], +// ); +// } +// } +// : uniqueCustomId = uniqueCustomId ?? const Uuid().v4() diff --git a/lib/pages/routiens/models/routine_model.dart b/lib/pages/routiens/models/routine_model.dart new file mode 100644 index 00000000..bb3e117b --- /dev/null +++ b/lib/pages/routiens/models/routine_model.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ScenesModel { + final String id; + final String? sceneTuyaId; + final String name; + final String status; + final String type; + final String? icon; + + ScenesModel({ + required this.id, + this.sceneTuyaId, + required this.name, + required this.status, + required this.type, + this.icon, + }); + + factory ScenesModel.fromRawJson(String str) => + ScenesModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Uint8List? get iconInBytes { + if (icon == null || icon?.isEmpty == true) return null; + try { + return base64Decode(icon!); + } catch (e) { + return null; + } + } + + factory ScenesModel.fromJson(Map json, + {bool? isAutomation}) { + return ScenesModel( + id: json["id"] ?? json["uuid"] ?? '', + sceneTuyaId: json["sceneTuyaId"] as String?, + name: json["name"] ?? '', + status: json["status"] ?? '', + type: json["type"] ?? '', + icon: + isAutomation == true ? Assets.automation : (json["icon"] as String?), + ); + } + + Map toJson() => { + "id": id, + "sceneTuyaId": sceneTuyaId ?? '', + "name": name, + "status": status, + "type": type, + }; +} diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart new file mode 100644 index 00000000..320eb15d --- /dev/null +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/widgets/conditions_routines_devices_view.dart'; +import 'package:syncrow_web/pages/routiens/widgets/if_container.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_search_and_buttons.dart'; +import 'package:syncrow_web/pages/routiens/widgets/then_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateNewRoutineView extends StatelessWidget { + final bool isUpdate; + final String? routineId; + final bool isScene; + + const CreateNewRoutineView({ + super.key, + this.isUpdate = false, + this.routineId, + this.isScene = true, + }); + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const RoutineSearchAndButtons(), + const SizedBox(height: 20), + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Card( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + ), + child: const ConditionsRoutinesDevicesView()), + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + children: [ + /// IF Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), + ), + ), + child: const IfContainer(), + ), + ), + ), + Container( + height: 2, + width: double.infinity, + color: ColorsManager.dialogBlueTitle, + ), + + /// THEN Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), + ), + ), + child: const ThenContainer(), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/routiens/view/effective_period_view.dart b/lib/pages/routiens/view/effective_period_view.dart new file mode 100644 index 00000000..5e6c33da --- /dev/null +++ b/lib/pages/routiens/view/effective_period_view.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/period_option.dart'; +import 'package:syncrow_web/pages/routiens/widgets/repeat_days.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EffectivePeriodView extends StatelessWidget { + const EffectivePeriodView({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Spacer(), + Expanded( + child: Text( + 'Effective Period', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + ), + const Spacer(), + ], + ), + const Divider( + color: ColorsManager.backgroundColor, + ), + const PeriodOptions( + showCustomTimePicker: EffectPeriodHelper.showCustomTimePicker, + ), + const SizedBox(height: 16), + const RepeatDays(), + const SizedBox(height: 24), + ], + ), + ); + } +} diff --git a/lib/pages/routiens/view/routines_view.dart b/lib/pages/routiens/view/routines_view.dart new file mode 100644 index 00000000..be5a5a3a --- /dev/null +++ b/lib/pages/routiens/view/routines_view.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; +import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/fetch_routine_scenes_automation.dart'; +import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/routine_view_card.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoutinesView extends StatelessWidget { + const RoutinesView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Create New Routines", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 10, + ), + RoutineViewCard( + onTap: () { + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(true), + ); + context.read().add( + (ResetRoutineState()), + ); + }, + icon: Icons.add, + textString: '', + ), + const SizedBox( + height: 15, + ), + const Expanded(child: FetchRoutineScenesAutomation()), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart new file mode 100644 index 00000000..5cc31bf3 --- /dev/null +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; +import 'package:syncrow_web/pages/routiens/widgets/scenes_and_automations.dart'; +import 'package:syncrow_web/pages/routiens/widgets/search_bar_condition_title.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ConditionsRoutinesDevicesView extends StatelessWidget { + const ConditionsRoutinesDevicesView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConditionTitleAndSearchBar(), + SizedBox(height: 10), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + deviceData: { + 'deviceId': 'tab_to_run', + 'type': 'trigger', + 'name': 'Tab to run', + }, + ), + DraggableCard( + imagePath: Assets.map, + title: 'Location', + deviceData: { + 'deviceId': 'location', + 'type': 'trigger', + 'name': 'Location', + }, + ), + DraggableCard( + imagePath: Assets.weather, + title: 'Weather', + deviceData: { + 'deviceId': 'weather', + 'type': 'trigger', + 'name': 'Weather', + }, + ), + DraggableCard( + imagePath: Assets.schedule, + title: 'Schedule', + deviceData: { + 'deviceId': 'schedule', + 'type': 'trigger', + 'name': 'Schedule', + }, + ), + ], + ), + SizedBox(height: 10), + TitleRoutine( + title: 'Conditions', + subtitle: '(THEN)', + ), + SizedBox(height: 10), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.notification, + title: 'Send Notification', + deviceData: { + 'deviceId': 'notification', + 'type': 'action', + 'name': 'Send Notification', + }, + ), + DraggableCard( + imagePath: Assets.delay, + title: 'Delay the action', + deviceData: { + 'deviceId': 'delay', + 'type': 'action', + 'name': 'Delay the action', + 'uniqueCustomId': '', + }, + ), + ], + ), + SizedBox(height: 10), + TitleRoutine( + title: 'Routines', + subtitle: '(THEN)', + ), + SizedBox(height: 10), + ScenesAndAutomations(), + SizedBox(height: 10), + TitleRoutine( + title: 'Devices', + subtitle: '', + ), + SizedBox(height: 10), + RoutineDevices(), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/dialog_footer.dart b/lib/pages/routiens/widgets/dialog_footer.dart new file mode 100644 index 00000000..15db9732 --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_footer.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogFooter extends StatelessWidget { + final VoidCallback onCancel; + final VoidCallback? onConfirm; + final bool isConfirmEnabled; + final int? dialogWidth; + + const DialogFooter({ + Key? key, + required this.onCancel, + required this.onConfirm, + required this.isConfirmEnabled, + this.dialogWidth, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.greyColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: _buildFooterButton( + context, + 'Cancel', + onCancel, + ), + ), + if (isConfirmEnabled) ...[ + Container(width: 1, height: 50, color: ColorsManager.greyColor), + Expanded( + child: _buildFooterButton( + context, + 'Confirm', + onConfirm, + ), + ), + ], + ], + ), + ); + } + + Widget _buildFooterButton( + BuildContext context, + String text, + VoidCallback? onTap, + ) { + return GestureDetector( + onTap: onTap, + child: SizedBox( + height: 50, + child: Center( + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: text == 'Confirm' + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/dialog_header.dart b/lib/pages/routiens/widgets/dialog_header.dart new file mode 100644 index 00000000..4fe1f0b1 --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_header.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogHeader extends StatelessWidget { + final String title; + + const DialogHeader(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 10, + ), + Text( + title, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart new file mode 100644 index 00000000..5b4de81f --- /dev/null +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -0,0 +1,182 @@ +import 'dart:convert'; + +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/models/device_functions.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DraggableCard extends StatelessWidget { + final String imagePath; + final String title; + final Map deviceData; + final EdgeInsetsGeometry? padding; + final void Function()? onRemove; + final bool? isFromThen; + final bool? isFromIf; + + const DraggableCard({ + super.key, + required this.imagePath, + required this.title, + required this.deviceData, + this.padding, + this.onRemove, + this.isFromThen, + this.isFromIf, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final deviceFunctions = + state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; + + return Draggable>( + data: deviceData, + feedback: Transform.rotate( + angle: -0.1, + child: + _buildCardContent(context, deviceFunctions, padding: padding), + ), + childWhenDragging: _buildGreyContainer(), + child: _buildCardContent(context, deviceFunctions, padding: padding), + ); + }, + ); + } + + Widget _buildCardContent( + BuildContext context, List deviceFunctions, + {EdgeInsetsGeometry? padding}) { + return Stack( + children: [ + Card( + color: ColorsManager.whiteColors, + child: Container( + padding: padding ?? const EdgeInsets.all(16), + width: 110, + height: deviceFunctions.isEmpty ? 123 : null, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, + ), + ), + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.memory( + base64Decode(imagePath), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], + ), + if (deviceFunctions.isNotEmpty) + // const Divider(height: 1), + ...deviceFunctions.map((function) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + '${function.operationName}: ${_formatFunctionValue(function)}', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 9, + color: ColorsManager.textGray, + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + )), + ], + ), + ), + ), + Positioned( + top: -4, + right: -6, + child: Visibility( + visible: (isFromIf ?? false) || (isFromThen ?? false), + child: IconButton( + onPressed: onRemove == null + ? null + : () { + onRemove!(); + }, + icon: const Icon( + Icons.close, + color: ColorsManager.boxColor, + ), + ), + ), + ), + ], + ); + } + + String _formatFunctionValue(DeviceFunctionData function) { + if (function.functionCode == 'temp_set' || + function.functionCode == 'temp_current') { + return '${(function.value / 10).toStringAsFixed(0)}°C'; + } else if (function.functionCode.contains('countdown')) { + final seconds = function.value.toInt(); + if (seconds >= 3600) { + final hours = (seconds / 3600).floor(); + final remainingMinutes = ((seconds % 3600) / 60).floor(); + final remainingSeconds = seconds % 60; + return '$hours h ${remainingMinutes}m ${remainingSeconds}s'; + } else if (seconds >= 60) { + final minutes = (seconds / 60).floor(); + final remainingSeconds = seconds % 60; + return '$minutes m ${remainingSeconds}s'; + } + return '${seconds}s'; + } + return function.value.toString(); + } + + Widget _buildGreyContainer() { + return Container( + height: 123, + width: 90, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(20), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart new file mode 100644 index 00000000..9c357a17 --- /dev/null +++ b/lib/pages/routiens/widgets/if_container.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:uuid/uuid.dart'; + +class IfContainer extends StatelessWidget { + const IfContainer({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return DragTarget>( + builder: (context, candidateData, rejectedData) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('IF', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + if (state.isAutomation && state.ifItems.isNotEmpty) + AutomationOperatorSelector( + selectedOperator: state.selectedAutomationOperator), + ], + ), + const SizedBox(height: 16), + if (state.isTabToRun) + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + deviceData: {}, + ), + ], + ), + if (!state.isTabToRun) + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + state.ifItems.length, + (index) => GestureDetector( + onTap: () async { + if (!state.isTabToRun) { + final result = await DeviceDialogHelper + .showDeviceDialog( + context, state.ifItems[index], + removeComparetors: false); + + if (result != null) { + context.read().add( + AddToIfContainer( + state.ifItems[index], false)); + } else if (![ + 'AC', + '1G', + '2G', + '3G' + ].contains( + state.ifItems[index]['productType'])) { + context.read().add( + AddToIfContainer( + state.ifItems[index], false)); + } + } + }, + child: DraggableCard( + imagePath: + state.ifItems[index]['imagePath'] ?? '', + title: state.ifItems[index]['title'] ?? '', + deviceData: state.ifItems[index], + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + isFromThen: false, + isFromIf: true, + onRemove: () { + context.read().add( + RemoveDragCard( + index: index, + isFromThen: false, + key: state.ifItems[index] + ['uniqueCustomId'])); + }, + ), + )), + ), + ], + ), + ); + }, + onAcceptWithDetails: (data) async { + final uniqueCustomId = const Uuid().v4(); + + final mutableData = Map.from(data.data); + mutableData['uniqueCustomId'] = uniqueCustomId; + + if (state.isAutomation && mutableData['deviceId'] == 'tab_to_run') { + return; + } + + if (!state.isTabToRun) { + if (mutableData['deviceId'] == 'tab_to_run') { + context + .read() + .add(AddToIfContainer(mutableData, true)); + } else { + final result = await DeviceDialogHelper.showDeviceDialog( + context, mutableData, + removeComparetors: false); + + if (result != null) { + context + .read() + .add(AddToIfContainer(mutableData, false)); + } else if (!['AC', '1G', '2G', '3G'] + .contains(mutableData['productType'])) { + context + .read() + .add(AddToIfContainer(mutableData, false)); + } + } + } + }, + ); + }, + ); + } +} + +class AutomationOperatorSelector extends StatelessWidget { + const AutomationOperatorSelector({ + super.key, + required this.selectedOperator, + }); + + final String selectedOperator; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: selectedOperator.toLowerCase() == 'or' + ? ColorsManager.dialogBlueTitle + : ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ), + ), + child: Text( + 'Any condition is met', + style: context.textTheme.bodyMedium?.copyWith( + color: selectedOperator.toLowerCase() == 'or' + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + ), + ), + onPressed: () { + context + .read() + .add(const ChangeAutomationOperator(operator: 'or')); + }, + ), + Container( + width: 3, + height: 24, + color: ColorsManager.dividerColor, + ), + TextButton( + style: TextButton.styleFrom( + backgroundColor: selectedOperator.toLowerCase() == 'and' + ? ColorsManager.dialogBlueTitle + : ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ), + ), + child: Text( + 'All condition is met', + style: context.textTheme.bodyMedium?.copyWith( + color: selectedOperator.toLowerCase() == 'and' + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + ), + ), + onPressed: () { + context + .read() + .add(const ChangeAutomationOperator(operator: 'and')); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/main_routine_view/fetch_routine_scenes_automation.dart b/lib/pages/routiens/widgets/main_routine_view/fetch_routine_scenes_automation.dart new file mode 100644 index 00000000..c9a5114f --- /dev/null +++ b/lib/pages/routiens/widgets/main_routine_view/fetch_routine_scenes_automation.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/main_routine_view/routine_view_card.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class FetchRoutineScenesAutomation extends StatefulWidget { + const FetchRoutineScenesAutomation({super.key}); + + @override + State createState() => + _FetchRoutineScenesState(); +} + +class _FetchRoutineScenesState extends State + with HelperResponsiveLayout { + @override + void initState() { + super.initState(); + context.read() + ..add(const LoadScenes(spaceId, communityId)) + ..add(const LoadAutomation(spaceId)); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Scenes (Tab to Run)", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + if (state.scenes.isEmpty) + Text( + "No scenes found", + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.grayColor, + ), + ), + if (state.scenes.isNotEmpty) + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: isSmallScreenSize(context) ? 160 : 170, + ), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: state.scenes.length, + itemBuilder: (context, index) => Padding( + padding: EdgeInsets.only( + right: isSmallScreenSize(context) ? 4.0 : 8.0, + ), + child: Stack( + children: [ + RoutineViewCard( + onTap: () {}, + textString: state.scenes[index].name, + icon: state.scenes[index].icon ?? + Assets.logoHorizontal, + isFromScenes: true, + iconInBytes: + state.scenes[index].iconInBytes, + ), + Positioned( + top: 0, + right: 0, + child: InkWell( + onTap: () => context + .read() + .add( + DeleteScene( + sceneId: state.scenes[index].id, + unitUuid: spaceId, + ), + ), + child: Container( + height: 20, + width: 20, + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.grayColor, + width: 2.0, + ), + ), + child: const Center( + child: Icon(Icons.delete, + size: 15, + color: ColorsManager.grayColor), + ), + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 15), + Text( + "Automations", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: ColorsManager.grayColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + if (state.automations.isEmpty) + Text( + "No automations found", + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.grayColor, + ), + ), + if (state.automations.isNotEmpty) + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: isSmallScreenSize(context) ? 160 : 170, + ), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: state.automations.length, + itemBuilder: (context, index) => Padding( + padding: EdgeInsets.only( + right: isSmallScreenSize(context) ? 4.0 : 8.0, + ), + child: Stack( + children: [ + RoutineViewCard( + onTap: () {}, + textString: state.automations[index].name, + icon: state.automations[index].icon ?? + Assets.automation, + ), + Positioned( + top: 0, + right: 0, + child: InkWell( + onTap: () => + context.read().add( + DeleteAutomation( + automationId: state + .automations[index].id, + unitUuid: spaceId, + ), + ), + child: Container( + height: 20, + width: 20, + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.grayColor, + width: 2.0, + ), + ), + child: const Center( + child: Icon(Icons.delete, + size: 15, + color: ColorsManager.grayColor), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/main_routine_view/routine_view_card.dart b/lib/pages/routiens/widgets/main_routine_view/routine_view_card.dart new file mode 100644 index 00000000..7e446507 --- /dev/null +++ b/lib/pages/routiens/widgets/main_routine_view/routine_view_card.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class RoutineViewCard extends StatelessWidget with HelperResponsiveLayout { + const RoutineViewCard({ + super.key, + required this.onTap, + required this.icon, + required this.textString, + this.isFromScenes, + this.iconInBytes, + }); + + final Function() onTap; + final dynamic icon; + final String textString; + final bool? isFromScenes; + final Uint8List? iconInBytes; + + @override + Widget build(BuildContext context) { + final double cardWidth = isSmallScreenSize(context) + ? 120 + : isMediumScreenSize(context) + ? 135 + : 150; + + final double cardHeight = isSmallScreenSize(context) ? 160 : 170; + + final double iconSize = isSmallScreenSize(context) + ? 50 + : isMediumScreenSize(context) + ? 60 + : 70; + + return ConstrainedBox( + constraints: BoxConstraints( + maxWidth: cardWidth, + maxHeight: cardHeight, + ), + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + color: ColorsManager.whiteColors, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Center( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(120), + border: Border.all( + color: ColorsManager.greyColor, + width: 2.0, + ), + ), + height: iconSize, + width: iconSize, + child: (isFromScenes ?? false) + ? (iconInBytes != null && + iconInBytes?.isNotEmpty == true) + ? Image.memory( + iconInBytes!, + height: iconSize, + width: iconSize, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + Image.asset( + Assets.logo, + height: iconSize, + width: iconSize, + fit: BoxFit.contain, + ), + ) + : Image.asset( + Assets.logo, + height: iconSize, + width: iconSize, + fit: BoxFit.contain, + ) + : (icon is String && icon.endsWith('.svg')) + ? SvgPicture.asset( + icon, + fit: BoxFit.contain, + ) + : Icon( + icon, + color: ColorsManager.dialogBlueTitle, + size: isSmallScreenSize(context) ? 30 : 40, + ), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + textString, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: isSmallScreenSize(context) ? 10 : 12, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/period_option.dart b/lib/pages/routiens/widgets/period_option.dart new file mode 100644 index 00000000..1871ebda --- /dev/null +++ b/lib/pages/routiens/widgets/period_option.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +class PeriodOptions extends StatelessWidget { + final Future?> Function(BuildContext) showCustomTimePicker; + + const PeriodOptions({ + required this.showCustomTimePicker, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildRadioOption(context, EnumEffectivePeriodOptions.allDay, '24 Hours'), + _buildRadioOption(context, EnumEffectivePeriodOptions.daytime, 'Sunrise to Sunset'), + _buildRadioOption(context, EnumEffectivePeriodOptions.night, 'Sunset to Sunrise'), + ListTile( + contentPadding: EdgeInsets.zero, + onTap: () => showCustomTimePicker(context), + title: Text( + 'Custom', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + subtitle: state.customStartTime != null && state.customEndTime != null + ? Text( + '${"${state.customStartTime}"} - ${"${state.customEndTime}"}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10), + ) + : Text( + '00:00 - 23:59', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10), + ), + trailing: Radio( + value: EnumEffectivePeriodOptions.custom, + groupValue: state.selectedPeriod, + onChanged: (value) async { + if (value != null) { + context.read().add(SetPeriod(value)); + } + showCustomTimePicker(context); + }, + ), + ), + ], + ); + }, + ); + } + + Widget _buildRadioOption( + BuildContext context, EnumEffectivePeriodOptions value, String subtitle) { + return BlocBuilder( + builder: (context, state) { + return ListTile( + contentPadding: EdgeInsets.zero, + onTap: () { + context.read().add(SetPeriod(value)); + }, + title: Text( + EffectPeriodHelper.formatEnumValue(value), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, fontWeight: FontWeight.w400, fontSize: 12), + ), + subtitle: Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 10), + ), + trailing: Radio( + value: value, + groupValue: state.selectedPeriod, + onChanged: (value) { + if (value != null) { + context.read().add(SetPeriod(value)); + } + }, + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/repeat_days.dart b/lib/pages/routiens/widgets/repeat_days.dart new file mode 100644 index 00000000..8ee92367 --- /dev/null +++ b/lib/pages/routiens/widgets/repeat_days.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RepeatDays extends StatelessWidget { + const RepeatDays({super.key}); + + @override + Widget build(BuildContext context) { + final effectiveBloc = context.read(); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Repeat', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 14)), + const SizedBox(width: 8), + BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: effectiveBloc.daysMap.entries.map((entry) { + final day = entry.key; + final abbreviation = entry.value; + final dayIndex = effectiveBloc.getDayIndex(day); + final isSelected = state.selectedDaysBinary[dayIndex] == '1'; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: GestureDetector( + onTap: () { + effectiveBloc.add(ToggleDay(day)); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.grey : Colors.grey.shade300, + width: 1, + ), + ), + child: CircleAvatar( + radius: 15, + backgroundColor: Colors.white, + child: Text( + abbreviation, + style: TextStyle( + fontSize: 16, + color: isSelected ? Colors.grey : Colors.grey.shade300, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + const SizedBox( + height: 8, + ), + if (state.selectedDaysBinary == '0000000') + Text( + 'At least one day must be selected', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart new file mode 100644 index 00000000..dbdf71f4 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -0,0 +1,77 @@ +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/models/devices_model.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; + +class RoutineDevices extends StatelessWidget { + const RoutineDevices({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DeviceManagementBloc()..add(FetchDevices()), + child: BlocBuilder( + builder: (context, state) { + if (state is DeviceManagementLoaded) { + List deviceList = state.devices + .where((device) => + device.productType == 'AC' || + device.productType == '1G' || + device.productType == '2G' || + device.productType == '3G') + .toList(); + + // Provide the RoutineBloc to the child widgets + return BlocBuilder( + builder: (context, routineState) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: deviceList.asMap().entries.map((entry) { + final device = entry.value; + if (routineState.searchText != null && routineState.searchText!.isNotEmpty) { + return device.name! + .toLowerCase() + .contains(routineState.searchText!.toLowerCase()) + ? DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'device': device, + 'imagePath': device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + 'uniqueCustomId': '', + }, + ) + : Container(); + } else { + return DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'device': device, + 'imagePath': device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + 'uniqueCustomId': '', + }, + ); + } + }).toList(), + ); + }, + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart new file mode 100644 index 00000000..39e342ff --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart @@ -0,0 +1,421 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; + +class ACHelper { + static Future?> showACFunctionsDialog( + BuildContext context, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String uniqueCustomId, + bool? removeComparetors, + ) async { + List acFunctions = functions.whereType().toList(); + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); + + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('AC Functions'), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Function list + SizedBox( + width: selectedFunction != null ? 320 : 360, + child: _buildFunctionsList( + context: context, + acFunctions: acFunctions, + onFunctionSelected: + (functionCode, operationName) => context + .read() + .add(SelectFunction( + functionCode: functionCode, + operationName: operationName, + )), + ), + ), + // Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + acFunctions: acFunctions, + device: device, + operationName: selectedOperationName ?? '', + removeComparators: removeComparetors, + ), + ), + ], + ), + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + ), + ); + }, + ).then((value) { + return value; + }); + } + + /// Build functions list for AC functions dialog + static Widget _buildFunctionsList({ + required BuildContext context, + required List acFunctions, + required Function(String, String) onFunctionSelected, + }) { + return ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: acFunctions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider( + color: ColorsManager.dividerColor, + ), + ), + itemBuilder: (context, index) { + final function = acFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => onFunctionSelected( + function.code, + function.operationName, + ), + ); + }, + ); + } + + /// Build value selector for AC functions dialog + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List acFunctions, + AllDevicesModel? device, + required String operationName, + bool? removeComparators, + }) { + if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildTemperatureSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + removeComparators: removeComparators, + ); + } + + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + /// Build temperature selector for AC functions dialog + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + bool? removeComparators, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (removeComparators != true) + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildTemperatureDisplay( + context, + initialValue, + device, + operationName, + selectedFunctionData, + selectCode, + ), + const SizedBox(height: 20), + _buildTemperatureSlider( + context, + initialValue, + device, + operationName, + selectedFunctionData, + selectCode, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + + // Function(String) onConditionChanged, + ) { + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: + conditions.map((c) => c == (currentCondition ?? "==")).toList(), + children: conditions.map((c) => Text(c)).toList(), + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildTemperatureDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${(initialValue ?? 200) / 10}°C', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildTemperatureSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + return Slider( + value: initialValue is int ? initialValue.toDouble() : 160.0, + min: 160, + max: 300, + divisions: 14, + label: '${((initialValue ?? 160) / 10).toInt()}°C', + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + + // required Function(dynamic) onValueChanged, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart new file mode 100644 index 00000000..06995882 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart @@ -0,0 +1,95 @@ +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/models/device_functions.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/constants/assets.dart'; + +class AutomationDialog extends StatefulWidget { + final String automationName; + final String automationId; + final String uniqueCustomId; + + const AutomationDialog({ + super.key, + required this.automationName, + required this.automationId, + required this.uniqueCustomId, + }); + + @override + _AutomationDialogState createState() => _AutomationDialogState(); +} + +class _AutomationDialogState extends State { + bool _isEnabled = true; + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + width: 400, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader(widget.automationName), + const SizedBox(height: 16), + ListTile( + leading: SvgPicture.asset(Assets.acPower, width: 24, height: 24), + title: const Text('Enable'), + trailing: Radio( + value: true, + groupValue: _isEnabled, + onChanged: (bool? value) { + setState(() { + _isEnabled = value!; + }); + }, + ), + ), + ListTile( + leading: + SvgPicture.asset(Assets.acPowerOff, width: 24, height: 24), + title: const Text('Disable'), + trailing: Radio( + value: false, + groupValue: _isEnabled, + onChanged: (bool? value) { + setState(() { + _isEnabled = value!; + }); + }, + ), + ), + const SizedBox(height: 16), + DialogFooter( + onConfirm: () { + context.read().add( + AddFunctionToRoutine( + [ + DeviceFunctionData( + entityId: widget.automationId, + functionCode: 'automation', + value: _isEnabled ? 'rule_enable' : 'rule_disable', + operationName: 'Automation', + ), + ], + widget.uniqueCustomId, + ), + ); + Navigator.of(context).pop(true); + }, + onCancel: () => Navigator.of(context).pop(false), + isConfirmEnabled: true, + dialogWidth: 400, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart new file mode 100644 index 00000000..6195a931 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart @@ -0,0 +1,85 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; + +class DelayHelper { + static Future?> showDelayPickerDialog( + BuildContext context, Map data) async { + int hours = 0; + int minutes = 0; + + return showDialog?>( + context: context, + builder: (BuildContext context) { + final routineBloc = context.read(); + int totalSec = 0; + + final selectedFunctionData = + routineBloc.state.selectedFunctions[data['uniqueCustomId']] ?? []; + + if (selectedFunctionData.isNotEmpty) { + totalSec = selectedFunctionData[0].value; + // Convert seconds to hours and minutes + hours = totalSec ~/ 3600; + minutes = (totalSec % 3600) ~/ 60; + } + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: 600, + height: 300, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('Select Delay Duration'), + Expanded( + child: CupertinoTimerPicker( + mode: CupertinoTimerPickerMode.hm, + initialTimerDuration: Duration(hours: hours, minutes: minutes), + onTimerDurationChanged: (Duration newDuration) { + hours = newDuration.inHours; + minutes = newDuration.inMinutes % 60; + }, + ), + ), + DialogFooter( + onCancel: () { + Navigator.of(context).pop(); + }, + onConfirm: () { + int totalSeconds = (hours * 3600) + (minutes * 60); + context.read().add(AddFunctionToRoutine( + [ + DeviceFunctionData( + entityId: 'delay', + functionCode: 'delay', + operationName: 'Delay', + value: totalSeconds, + ) + ], + data['uniqueCustomId'], + )); + + Navigator.pop(context, { + 'deviceId': 'delay', + 'value': totalSeconds, + }); + }, + isConfirmEnabled: true, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/discard_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/discard_dialog.dart new file mode 100644 index 00000000..4c8e4688 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/discard_dialog.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DiscardDialog { + static void show(BuildContext context) { + context.customAlertDialog( + alertBody: Container( + height: 150, + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + 'If you close, you will lose all the changes you have made.', + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const SizedBox( + height: 20, + ), + Text( + 'Are you sure you wish to close?', + style: context.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + ), + ) + ], + )), + title: 'Discard', + titleStyle: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.bold, + ), + onDismissText: "Don’t Close", + onConfirmText: "Close", + onDismissColor: ColorsManager.grayColor, + onConfirmColor: ColorsManager.red.withOpacity(0.8), + onDismiss: () { + Navigator.pop(context); + }, + onConfirm: () { + context.read().add(ResetRoutineState()); + Navigator.pop(context); + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(false), + ); + BlocProvider.of(context).add( + const TriggerSwitchTabsEvent(true), + ); + }); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart new file mode 100644 index 00000000..70a6776d --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:time_picker_spinner/time_picker_spinner.dart'; + +class EffectPeriodHelper { + static Future?> showCustomTimePicker(BuildContext context) async { + String selectedStartTime = "00:00"; + String selectedEndTime = "23:59"; + PageController pageController = PageController(initialPage: 0); + + DateTime startDateTime = DateTime(2022, 1, 1, 0, 0); + DateTime endDateTime = DateTime(2022, 1, 1, 23, 59); + + context.customAlertDialog( + alertBody: SizedBox( + height: 250, + child: PageView( + controller: pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildTimePickerPage( + context: context, + pageController: pageController, + isStartTime: true, + time: startDateTime, + onTimeChange: (time) { + selectedStartTime = + "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"; + }, + ), + _buildTimePickerPage( + context: context, + pageController: pageController, + isStartTime: false, + time: endDateTime, + onTimeChange: (time) { + selectedEndTime = + "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"; + }, + ), + ], + ), + ), + title: "Custom", + onConfirm: () { + context.read().add( + SetCustomTime(selectedStartTime, selectedEndTime), + ); + context.read().add( + const SetPeriod(EnumEffectivePeriodOptions.custom), + ); + + Navigator.of(context).pop(); + }, + ); + return null; + } + + static Widget _buildTimePickerPage({ + required BuildContext context, + required PageController pageController, + required bool isStartTime, + required DateTime time, + required Function(DateTime) onTimeChange, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!isStartTime) + TextButton( + onPressed: () { + pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + child: const Text('Start'), + ), + TextButton( + onPressed: () {}, + child: Text(isStartTime ? "Start" : "End", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10)), + ), + if (isStartTime) + TextButton( + onPressed: () { + pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + child: Text('End', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10)), + ), + ], + ), + ), + TimePickerSpinner( + is24HourMode: false, + normalTextStyle: const TextStyle( + fontSize: 24, + color: Colors.grey, + ), + highlightedTextStyle: const TextStyle( + fontSize: 24, + color: ColorsManager.primaryColor, + ), + spacing: 20, + itemHeight: 50, + isForce2Digits: true, + time: time, + onTimeChange: onTimeChange, + ), + const SizedBox(height: 16), + ], + ); + } + + static String formatEnumValue(EnumEffectivePeriodOptions value) { + switch (value) { + case EnumEffectivePeriodOptions.allDay: + return "All Day"; + case EnumEffectivePeriodOptions.daytime: + return "Daytime"; + case EnumEffectivePeriodOptions.night: + return "Night"; + case EnumEffectivePeriodOptions.custom: + return "Custom"; + case EnumEffectivePeriodOptions.none: + return "None"; + default: + return ""; + } + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart new file mode 100644 index 00000000..e7d7209f --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart @@ -0,0 +1,397 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/duration_format_helper.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class OneGangSwitchHelper { + static Future?> showSwitchFunctionsDialog( + BuildContext context, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String uniqueCustomId, + bool removeComparetors, + ) async { + List acFunctions = + functions.whereType().toList(); + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('1 Gang Light Switch Condition'), + Expanded( + child: Row( + children: [ + // Left side: Function list + Expanded( + child: ListView.separated( + itemCount: acFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = acFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () { + context + .read() + .add(SelectFunction( + functionCode: function.code, + operationName: + function.operationName, + )); + }, + ); + }, + ), + ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + acFunctions: acFunctions, + device: device, + operationName: selectedOperationName ?? '', + removeComparetors: removeComparetors, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + // for (var function in state.addedFunctions) { + // context.read().add( + // AddFunctionToRoutine( + // function, + // uniqueCustomId, + // ), + // ); + // } + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + )); + }, + ); + } + + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List acFunctions, + AllDevicesModel? device, + required String operationName, + required bool removeComparetors, + }) { + if (selectedFunction == 'countdown_1') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildCountDownSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + removeComparetors: removeComparetors, + ); + } + + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildCountDownSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + required bool removeComparetors, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (removeComparetors != true) + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildCountDownDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + const SizedBox(height: 20), + _buildCountDownSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, + ) { + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: + conditions.map((c) => c == (currentCondition ?? "==")).toList(), + children: conditions.map((c) => Text(c)).toList(), + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildCountDownDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildCountDownSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + final operationalValues = SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 86400, + stepValue: 1, + ); + return Slider( + value: (initialValue ?? 0).toDouble(), + min: operationalValues.minValue?.toDouble() ?? 0.0, + max: operationalValues.maxValue?.toDouble() ?? 0.0, + divisions: (((operationalValues.maxValue ?? 0) - + (operationalValues.minValue ?? 0)) / + (operationalValues.stepValue ?? 1)) + .round(), + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart new file mode 100644 index 00000000..7fc723af --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_state.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; +import 'package:syncrow_web/pages/routiens/view/effective_period_view.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:flutter/cupertino.dart'; + +class SettingHelper { + static Future showSettingDialog({ + required BuildContext context, + String? iconId, + }) async { + return showDialog( + context: context, + builder: (BuildContext context) { + final isAutomation = context.read().state.isAutomation; + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => EffectPeriodBloc(), + ), + BlocProvider( + create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? ''))), + ], + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, effectPeriodState) { + return BlocBuilder( + builder: (context, settingState) { + String selectedIcon = ''; + List list = []; + if (settingState is TabToRunSettingLoaded) { + selectedIcon = settingState.selectedIcon; + list = settingState.iconList; + } + return Container( + width: context.read().isExpanded ? 800 : 400, + height: context.read().isExpanded && isAutomation ? 500 : 300, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DialogHeader('Settings'), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 400, + child: isAutomation + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Column( + children: [ + InkWell( + onTap: () {}, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Validity', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + const Icon( + Icons.arrow_forward_ios_outlined, + color: ColorsManager.textGray, + size: 15, + ) + ], + ), + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + InkWell( + onTap: () { + BlocProvider.of(context).add( + FetchIcons( + expanded: !context + .read() + .isExpanded)); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Effective Period', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + const Icon( + Icons.arrow_forward_ios_outlined, + color: ColorsManager.textGray, + size: 15, + ) + ], + ), + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Executed by', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + Text('Cloud', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textGray, + fontWeight: FontWeight.w400, + fontSize: 14)), + ], + ), + ], + )), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Column( + children: [ + InkWell( + onTap: () { + BlocProvider.of(context).add( + FetchIcons( + expanded: !context + .read() + .isExpanded)); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Icons', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + const Icon( + Icons.arrow_forward_ios_outlined, + color: ColorsManager.textGray, + size: 15, + ) + ], + ), + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Show on devices page', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + height: 30, + width: 1, + color: ColorsManager.graysColor, + ), + Transform.scale( + scale: .8, + child: CupertinoSwitch( + value: true, + onChanged: (value) {}, + applyTheme: true, + ), + ), + ], + ) + ], + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Executed by', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + Text('Cloud', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textGray, + fontWeight: FontWeight.w400, + fontSize: 14)), + ], + ), + ], + )), + ], + ), + ), + if (context.read().isExpanded && !isAutomation) + SizedBox( + width: 400, + height: 150, + child: settingState is LoadingState + ? const Center(child: CircularProgressIndicator()) + : GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + shrinkWrap: true, + itemCount: list.length, + itemBuilder: (context, index) { + final iconModel = list[index]; + return SizedBox( + width: 35, + height: 35, + child: InkWell( + onTap: () { + BlocProvider.of(context) + .add(SelectIcon( + iconId: iconModel.uuid, + )); + selectedIcon = iconModel.uuid; + }, + child: SizedBox( + child: ClipOval( + child: Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + border: Border.all( + color: selectedIcon == iconModel.uuid + ? ColorsManager + .primaryColorWithOpacity + : Colors.transparent, + width: 2, + ), + shape: BoxShape.circle, + ), + child: Image.memory( + iconModel.iconBytes, + ), + ), + ), + ), + ), + ); + }, + )), + if (context.read().isExpanded && isAutomation) + const SizedBox(height: 350, width: 400, child: EffectivePeriodView()) + ], + ), + Container( + width: MediaQuery.sizeOf(context).width, + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.greyColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + alignment: AlignmentDirectional.center, + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textGray, + ), + ), + ), + ), + ), + Container(width: 1, height: 50, color: ColorsManager.greyColor), + Expanded( + child: InkWell( + onTap: () { + if (isAutomation) { + BlocProvider.of(context).add( + EffectiveTimePeriodEvent(EffectiveTime( + start: effectPeriodState.customStartTime!, + end: effectPeriodState.customEndTime!, + loops: effectPeriodState.selectedDaysBinary))); + Navigator.of(context).pop(); + } else { + Navigator.of(context).pop(selectedIcon); + } + }, + child: Container( + alignment: AlignmentDirectional.center, + child: Text( + 'Confirm', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ), + ), + ], + ), + ) + ], + ), + ); + }, + ); + }), + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart new file mode 100644 index 00000000..bb23ece4 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart @@ -0,0 +1,399 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/duration_format_helper.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ThreeGangSwitchHelper { + static Future?> showSwitchFunctionsDialog( + BuildContext context, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String uniqueCustomId, + bool removeComparetors, + ) async { + List switchFunctions = + functions.whereType().toList(); + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('3 Gangs Light Switch Condition'), + Expanded( + child: Row( + children: [ + // Left side: Function list + Expanded( + child: ListView.separated( + itemCount: switchFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () { + context + .read() + .add(SelectFunction( + functionCode: function.code, + operationName: + function.operationName, + )); + }, + ); + }, + ), + ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + switchFunctions: switchFunctions, + device: device, + operationName: selectedOperationName ?? '', + removeComparetors: removeComparetors, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + // for (var function in state.addedFunctions) { + // context.read().add( + // AddFunctionToRoutine( + // function, + // uniqueCustomId, + // ), + // ); + // } + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + )); + }, + ); + } + + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List switchFunctions, + AllDevicesModel? device, + required String operationName, + required bool removeComparetors, + }) { + if (selectedFunction == 'countdown_1' || + selectedFunction == 'countdown_2' || + selectedFunction == 'countdown_3') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildTemperatureSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + removeComparetors: removeComparetors, + ); + } + + final selectedFn = + switchFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + bool? removeComparetors, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (removeComparetors != true) + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildCountDownDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + const SizedBox(height: 20), + _buildCountDownSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, + ) { + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: + conditions.map((c) => c == (currentCondition ?? "==")).toList(), + children: conditions.map((c) => Text(c)).toList(), + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildCountDownDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildCountDownSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + final operationalValues = SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 86400, + stepValue: 1, + ); + return Slider( + value: (initialValue ?? 0).toDouble(), + min: operationalValues.minValue?.toDouble() ?? 0.0, + max: operationalValues.maxValue?.toDouble() ?? 0.0, + divisions: (((operationalValues.maxValue ?? 0) - + (operationalValues.minValue ?? 0)) / + (operationalValues.stepValue ?? 1)) + .round(), + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart new file mode 100644 index 00000000..f2a0ebd0 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart @@ -0,0 +1,398 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/duration_format_helper.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class TwoGangSwitchHelper { + static Future?> showSwitchFunctionsDialog( + BuildContext context, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String uniqueCustomId, + bool removeComparetors, + ) async { + List switchFunctions = + functions.whereType().toList(); + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('2 Gangs Light Switch Condition'), + Expanded( + child: Row( + children: [ + // Left side: Function list + Expanded( + child: ListView.separated( + itemCount: switchFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () { + context + .read() + .add(SelectFunction( + functionCode: function.code, + operationName: + function.operationName, + )); + }, + ); + }, + ), + ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + switchFunctions: switchFunctions, + device: device, + operationName: selectedOperationName ?? '', + removeComparetors: removeComparetors, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + // for (var function in state.addedFunctions) { + // context.read().add( + // AddFunctionToRoutine( + // function, + // uniqueCustomId + // ), + // ); + // } + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + )); + }, + ); + } + + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List switchFunctions, + AllDevicesModel? device, + required String operationName, + required bool removeComparetors, + }) { + if (selectedFunction == 'countdown_1' || + selectedFunction == 'countdown_2') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildTemperatureSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + removeComparetors: removeComparetors, + ); + } + + final selectedFn = + switchFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + bool? removeComparetors, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (removeComparetors != true) + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildCountDownDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + const SizedBox(height: 20), + _buildCountDownSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, + ) { + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: + conditions.map((c) => c == (currentCondition ?? "==")).toList(), + children: conditions.map((c) => Text(c)).toList(), + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildCountDownDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildCountDownSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + final operationalValues = SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 86400, + stepValue: 1, + ); + return Slider( + value: (initialValue ?? 0).toDouble(), + min: operationalValues.minValue?.toDouble() ?? 0.0, + max: operationalValues.maxValue?.toDouble() ?? 0.0, + divisions: (((operationalValues.maxValue ?? 0) - + (operationalValues.minValue ?? 0)) / + (operationalValues.stepValue ?? 1)) + .round(), + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart new file mode 100644 index 00000000..cdc560f6 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.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/widgets/routine_dialogs/discard_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/setting_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'; + +class RoutineSearchAndButtons extends StatelessWidget { + const RoutineSearchAndButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Wrap( + runSpacing: 16, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Wrap( + spacing: 12, + runSpacing: 12, + crossAxisAlignment: WrapCrossAlignment.end, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), + // child: StatefulTextField( + // title: 'Routine Name', + // initialValue: state.routineName ?? '', + // height: 40, + // // controller: TextEditingController(), + // hintText: 'Please enter the name', + // boxDecoration: containerWhiteDecoration, + // elevation: 0, + // borderRadius: 15, + // isRequired: true, + // width: 450, + // onSubmitted: (value) { + // // context.read().add(SetRoutineName(value)); + // }, + // onChanged: (value) { + // context.read().add(SetRoutineName(value)); + // }, + // ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('* ', + style: context.textTheme.bodyMedium! + .copyWith(color: ColorsManager.red, fontSize: 13)), + Text( + 'Routine Name', + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 13, + fontWeight: FontWeight.w600, + color: ColorsManager.blackColor, + ), + ), + ], + ), + Container( + width: 450, + height: 40, + decoration: containerWhiteDecoration, + child: TextFormField( + style: context.textTheme.bodyMedium! + .copyWith(color: ColorsManager.blackColor), + initialValue: state.routineName, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: context.textTheme.bodyMedium! + .copyWith(fontSize: 12, color: ColorsManager.grayColor), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + border: InputBorder.none, + ), + onChanged: (value) { + context.read().add(SetRoutineName(value)); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'This field is required'; + } + return null; + }, + ), + ), + ], + ), + ), + (constraints.maxWidth <= 1000) + ? const SizedBox() + : SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: state.isAutomation || state.isTabToRun + ? () async { + final result = await SettingHelper.showSettingDialog( + context: context, + ); + if (result != null) { + context + .read() + .add(AddSelectedIcon(result)); + } + } + : null, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.primaryColor, + ), + ), + ), + ), + ), + ], + ), + ), + if (constraints.maxWidth > 1000) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () { + DiscardDialog.show(context); + }, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Cancel', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () async { + if (state.routineName == null || state.routineName!.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Please enter the routine name'), + duration: const Duration(seconds: 2), + backgroundColor: ColorsManager.red, + action: SnackBarAction( + label: 'Dismiss', + onPressed: () { + // Optional action on Snackbar + }, + ), + ), + ); + return; + } + + if (state.ifItems.isEmpty || state.thenItems.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Please add if and then condition'), + duration: const Duration(seconds: 2), + backgroundColor: ColorsManager.red, + action: SnackBarAction( + label: 'Dismiss', + onPressed: () { + // Optional action on Snackbar + }, + ), + ), + ); + return; + } + final result = + await SaveRoutineHelper.showSaveRoutineDialog(context); + if (result != null && result) { + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(false), + ); + BlocProvider.of(context).add( + const TriggerSwitchTabsEvent(true), + ); + } + }, + borderRadius: 15, + elevation: 0, + backgroundColor: ColorsManager.primaryColor, + child: const Text( + 'Save', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.whiteColors, + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (constraints.maxWidth <= 1000) + Wrap( + runSpacing: 12, + children: [ + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: state.isAutomation || state.isTabToRun + ? () async { + final result = await SettingHelper.showSettingDialog( + context: context, + ); + if (result != null) { + context.read().add(AddSelectedIcon(result)); + } + } + : null, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.primaryColor, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () { + DiscardDialog.show(context); + }, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Cancel', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () { + if (state.routineName == null || state.routineName!.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Please enter the routine name'), + duration: const Duration(seconds: 2), + backgroundColor: ColorsManager.red, + action: SnackBarAction( + label: 'Dismiss', + onPressed: () { + // Optional action on Snackbar + }, + ), + ), + ); + return; + } + + if (state.ifItems.isEmpty || state.thenItems.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Please add if and then condition'), + duration: const Duration(seconds: 2), + backgroundColor: ColorsManager.red, + action: SnackBarAction( + label: 'Dismiss', + onPressed: () { + // Optional action on Snackbar + }, + ), + ), + ); + return; + } + SaveRoutineHelper.showSaveRoutineDialog(context); + }, + borderRadius: 15, + elevation: 0, + backgroundColor: ColorsManager.primaryColor, + child: const Text( + 'Save', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.whiteColors, + ), + ), + ), + ), + ), + ], + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routines_title_widget.dart b/lib/pages/routiens/widgets/routines_title_widget.dart new file mode 100644 index 00000000..7d2a272f --- /dev/null +++ b/lib/pages/routiens/widgets/routines_title_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class TitleRoutine extends StatelessWidget { + const TitleRoutine({ + super.key, + required this.title, + required this.subtitle, + }); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: context.textTheme.titleLarge?.copyWith( + color: ColorsManager.greyColor, + ), + ), + const SizedBox( + width: 4, + ), + Text( + subtitle, + style: context.textTheme.titleLarge?.copyWith( + color: ColorsManager.greyColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart new file mode 100644 index 00000000..a0bd1ed6 --- /dev/null +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ScenesAndAutomations extends StatefulWidget { + const ScenesAndAutomations({ + super.key, + }); + + @override + State createState() => _ScenesAndAutomationsState(); +} + +class _ScenesAndAutomationsState extends State { + @override + void initState() { + super.initState(); + context.read() + ..add(const LoadScenes(spaceId, communityId)) + ..add(const LoadAutomation(spaceId)); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (!state.isLoading) { + var scenes = [...state.scenes, ...state.automations]; + return Wrap( + spacing: 10, + runSpacing: 10, + children: scenes.asMap().entries.map((entry) { + final scene = entry.value; + if (state.searchText != null && state.searchText!.isNotEmpty) { + return scene.name + .toLowerCase() + .contains(state.searchText!.toLowerCase()) + ? DraggableCard( + imagePath: scene.icon ?? Assets.loginLogo, + title: scene.name, + deviceData: { + 'deviceId': scene.id, + 'name': scene.name, + 'status': scene.status, + 'type': scene.type, + 'icon': scene.icon, + }, + ) + : Container(); + } else { + return DraggableCard( + imagePath: scene.icon ?? Assets.loginLogo, + title: scene.name, + deviceData: { + 'deviceId': scene.id, + 'name': scene.name, + 'status': scene.status, + 'type': scene.type, + 'icon': scene.icon, + }, + ); + } + }).toList(), + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/search_bar_condition_title.dart b/lib/pages/routiens/widgets/search_bar_condition_title.dart new file mode 100644 index 00000000..c009f040 --- /dev/null +++ b/lib/pages/routiens/widgets/search_bar_condition_title.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/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 { + const ConditionTitleAndSearchBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final isMedium = isMediumScreenSize(context); + final isSmall = isSmallScreenSize(context); + return isMedium || isSmall + ? Wrap( + spacing: 10, + runSpacing: 10, + children: [ + const TitleRoutine(title: 'Conditions', subtitle: '(IF)'), + StatefulTextField( + title: '', + width: 250, + height: 40, + hintText: 'Search', + elevation: 0, + borderRadius: 15, + icon: Icons.search, + hintColor: ColorsManager.grayColor, + boxDecoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(15), + ), + controller: TextEditingController(), + // onSubmitted: (value) { + // context.read().add(SearchRoutines(value)); + // }, + onChanged: (value) { + context.read().add(SearchRoutines(value)); + }, + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const TitleRoutine(title: 'Conditions', subtitle: '(IF)'), + StatefulTextField( + title: '', + width: 250, + height: 40, + hintText: 'Search', + elevation: 0, + borderRadius: 15, + icon: Icons.search, + hintColor: ColorsManager.grayColor, + boxDecoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(15), + ), + controller: TextEditingController(), + // onSubmitted: (value) { + // context.read().add(SearchRoutines(value)); + // }, + onChanged: (value) { + context.read().add(SearchRoutines(value)); + }, + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart new file mode 100644 index 00000000..9491e8fd --- /dev/null +++ b/lib/pages/routiens/widgets/then_container.dart @@ -0,0 +1,173 @@ +// lib/pages/routiens/widgets/then_container.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/automation_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/delay_dialog.dart'; +import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:uuid/uuid.dart'; + +class ThenContainer extends StatelessWidget { + const ThenContainer({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return DragTarget>( + builder: (context, candidateData, rejectedData) { + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('THEN', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + state.thenItems.length, + (index) => GestureDetector( + onTap: () async { + if (state.thenItems[index]['deviceId'] == + 'delay') { + final result = await DelayHelper + .showDelayPickerDialog( + context, state.thenItems[index]); + + if (result != null) { + context + .read() + .add(AddToThenContainer({ + ...state.thenItems[index], + 'imagePath': Assets.delay, + 'title': 'Delay', + })); + } + return; + } + + final result = await DeviceDialogHelper + .showDeviceDialog( + context, state.thenItems[index], + removeComparetors: true); + + if (result != null) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } else if (!['AC', '1G', '2G', '3G'] + .contains(state.thenItems[index] + ['productType'])) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } + }, + child: DraggableCard( + imagePath: state.thenItems[index] + ['imagePath'] ?? + '', + title: + state.thenItems[index]['title'] ?? '', + deviceData: state.thenItems[index], + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + isFromThen: true, + isFromIf: false, + onRemove: () { + context.read().add( + RemoveDragCard( + index: index, + isFromThen: true, + key: state.thenItems[index] + ['uniqueCustomId'])); + }, + ), + ))), + ], + ), + ), + ); + }, + onAcceptWithDetails: (data) async { + final uniqueCustomId = const Uuid().v4(); + final mutableData = Map.from(data.data); + mutableData['uniqueCustomId'] = uniqueCustomId; + + if (mutableData['type'] == 'scene') { + context.read().add(AddToThenContainer(mutableData)); + return; + } + + if (mutableData['type'] == 'automation') { + final result = await showDialog( + context: context, + builder: (BuildContext context) => AutomationDialog( + automationName: mutableData['name'] ?? 'Automation', + automationId: mutableData['deviceId'] ?? '', + uniqueCustomId: uniqueCustomId, + ), + ); + + if (result != null) { + context.read().add(AddToThenContainer({ + ...mutableData, + 'imagePath': Assets.automation, + 'title': mutableData['name'], + })); + } + return; + } + + if (mutableData['type'] == 'tap_to_run' && state.isAutomation) { + context.read().add(AddToThenContainer({ + ...mutableData, + 'imagePath': Assets.logo, + 'title': mutableData['name'], + })); + + return; + } + + if (mutableData['type'] == 'tap_to_run' && !state.isAutomation) { + return; + } + + if (mutableData['deviceId'] == 'delay') { + final result = + await DelayHelper.showDelayPickerDialog(context, mutableData); + + if (result != null) { + context.read().add(AddToThenContainer({ + ...mutableData, + 'imagePath': Assets.delay, + 'title': 'Delay', + })); + } + return; + } + + final result = await DeviceDialogHelper.showDeviceDialog( + context, mutableData, + removeComparetors: true); + if (result != null) { + context.read().add(AddToThenContainer(mutableData)); + } else if (!['AC', '1G', '2G', '3G'] + .contains(mutableData['productType'])) { + context.read().add(AddToThenContainer(mutableData)); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart b/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart index 0a16b6c9..135ada3b 100644 --- a/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart +++ b/lib/pages/spaces_management/widgets/dialogs/create_community_dialog.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CreateCommunityDialog extends StatefulWidget { - final Function(String name, String description) - onCreateCommunity; + final Function(String name, String description) onCreateCommunity; const CreateCommunityDialog({super.key, required this.onCreateCommunity}); @@ -65,23 +64,20 @@ class CreateCommunityDialogState extends State { fillColor: ColorsManager.boxColor, hintStyle: const TextStyle( fontSize: 14, - color: ColorsManager.boxColor, + color: ColorsManager.grayBorder, fontWeight: FontWeight.w400, ), border: OutlineInputBorder( - borderSide: - const BorderSide(color: ColorsManager.boxColor, width: 1), + borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1), borderRadius: BorderRadius.circular(10), ), enabledBorder: OutlineInputBorder( - borderSide: - const BorderSide(color: ColorsManager.boxColor, width: 1), + borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1), borderRadius: BorderRadius.circular(10), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: ColorsManager.boxColor, width: 1), + borderSide: const BorderSide(color: ColorsManager.boxColor, width: 1), ), ), ), diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart new file mode 100644 index 00000000..333156ba --- /dev/null +++ b/lib/services/routines_api.dart @@ -0,0 +1,250 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; +import 'package:syncrow_web/pages/routiens/models/routine_details_model.dart'; +import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class SceneApi { + static final HTTPService _httpService = HTTPService(); + +// //create scene + static Future> createScene( + CreateSceneModel createSceneModel) async { + try { + debugPrint('create scene model: ${createSceneModel.toMap()}'); + final response = await _httpService.post( + path: ApiEndpoints.createScene, + body: createSceneModel.toMap(), + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + debugPrint('create scene response: $response'); + return response; + } catch (e) { + debugPrint(e.toString()); + rethrow; + } + } + +// +// create automation + static Future> createAutomation( + CreateAutomationModel createAutomationModel) async { + try { + debugPrint("automation body ${createAutomationModel.toMap()}"); + final response = await _httpService.post( + path: ApiEndpoints.createAutomation, + body: createAutomationModel.toMap(), + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + debugPrint('create automation response: $response'); + return response; + } catch (e) { + debugPrint(e.toString()); + rethrow; + } + } + + static Future> getIcon() async { + final response = await _httpService.get( + path: ApiEndpoints.getIconScene, + showServerMessage: false, + expectedResponseModel: (json) { + List iconsList = []; + json.forEach((element) { + iconsList.add(IconModel.fromJson(element)); + }); + return iconsList; + }, + ); + return response; + } + + //get scenes by community id and space id + + static Future> getScenesByUnitId( + String unitId, String communityId, + {showInDevice = false}) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getUnitScenes + .replaceAll('{spaceUuid}', unitId) + .replaceAll('{communityUuid}', communityId), + queryParameters: {'showInHomePage': showInDevice}, + showServerMessage: false, + expectedResponseModel: (json) { + final scenesJson = json['data'] as List; + + List scenes = []; + for (var scene in scenesJson) { + scenes.add(ScenesModel.fromJson(scene, isAutomation: false)); + } + return scenes; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + //getAutomation + + static Future> getAutomationByUnitId(String unitId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getSpaceAutomation.replaceAll('{unitUuid}', unitId), + showServerMessage: false, + expectedResponseModel: (json) { + List scenes = []; + for (var scene in json) { + scenes.add(ScenesModel.fromJson(scene, isAutomation: true)); + } + return scenes; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + // static Future triggerScene(String sceneId) async { + // try { + // final response = await _httpService.post( + // path: ApiEndpoints.triggerScene.replaceAll('{sceneId}', sceneId), + // showServerMessage: false, + // expectedResponseModel: (json) => json['success'], + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + +//automation details + static Future getAutomationDetails( + String automationId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getAutomationDetails + .replaceAll('{automationId}', automationId), + showServerMessage: false, + expectedResponseModel: (json) => RoutineDetailsModel.fromJson(json), + ); + return response; + } catch (e) { + rethrow; + } + } +// +// //updateAutomationStatus +// static Future updateAutomationStatus(String automationId, +// AutomationStatusUpdate createAutomationEnable) async { +// try { +// final response = await _httpService.put( +// path: ApiEndpoints.updateAutomationStatus +// .replaceAll('{automationId}', automationId), +// body: createAutomationEnable.toMap(), +// expectedResponseModel: (json) => json['success'], +// ); +// return response; +// } catch (e) { +// rethrow; +// } +// } + + //getScene + static Future getSceneDetails(String sceneId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getScene.replaceAll('{sceneId}', sceneId), + showServerMessage: false, + expectedResponseModel: (json) => RoutineDetailsModel.fromJson(json), + ); + return response; + } catch (e) { + rethrow; + } + } + // + // //update Scene + // static updateScene(CreateSceneModel createSceneModel, String sceneId) async { + // try { + // final response = await _httpService.put( + // path: ApiEndpoints.updateScene.replaceAll('{sceneId}', sceneId), + // body: createSceneModel + // .toJson(sceneId.isNotEmpty == true ? sceneId : null), + // expectedResponseModel: (json) { + // return json; + // }, + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + // + // //update automation + // static updateAutomation( + // CreateAutomationModel createAutomationModel, String automationId) async { + // try { + // final response = await _httpService.put( + // path: ApiEndpoints.updateAutomation + // .replaceAll('{automationId}', automationId), + // body: createAutomationModel + // .toJson(automationId.isNotEmpty == true ? automationId : null), + // expectedResponseModel: (json) { + // return json; + // }, + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + // + + + //delete Scene + static Future deleteScene( + {required String unitUuid, required String sceneId}) async { + try { + final response = await _httpService.delete( + path: ApiEndpoints.deleteScene + .replaceAll('{sceneId}', sceneId) + .replaceAll('{unitUuid}', unitUuid), + showServerMessage: false, + expectedResponseModel: (json) => json['statusCode'] == 200, + ); + return response; + } catch (e) { + rethrow; + } + } + + // delete automation + static Future deleteAutomation( + {required String unitUuid, required String automationId}) async { + try { + final response = await _httpService.delete( + path: ApiEndpoints.deleteAutomation + .replaceAll('{automationId}', automationId) + .replaceAll('{unitUuid}', unitUuid), + showServerMessage: false, + expectedResponseModel: (json) => json['statusCode'] == 200, + ); + return response; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 709d4ac7..95d0f214 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -53,6 +53,6 @@ abstract class ColorsManager { static const Color neutralGray = Color(0xFFE5E5E5); static const Color warningRed = Color(0xFFFF6465); static const Color borderColor = Color(0xFFE5E5E5); + static const Color CircleImageBackground = Color(0xFFF4F4F4); } //background: #background: #5D5D5D; - diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index b4bd575f..bb37fd60 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -11,14 +11,12 @@ 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'; @@ -35,50 +33,45 @@ abstract class ApiEndpoints { static const String gatewayApi = '/device/gateway/{gatewayUuid}/devices'; static const String openDoorLock = '/door-lock/open/{doorLockUuid}'; - static const String getDeviceLogs = - '/device/report-logs/{uuid}?code={code}'; + static const String getDeviceLogs = '/device/report-logs/{uuid}?code={code}'; // Space Module - static const String createSpace = - '/communities/{communityId}/spaces'; - static const String listSpaces = - '/communities/{communityId}/spaces'; - static const String deleteSpace = - '/communities/{communityId}/spaces/{spaceId}'; - static const String updateSpace = - '/communities/{communityId}/spaces/{spaceId}'; - static const String getSpace = - '/communities/{communityId}/spaces/{spaceId}'; - static const String getSpaceHierarchy = - '/communities/{communityId}/spaces'; + static const String createSpace = '/communities/{communityId}/spaces'; + static const String listSpaces = '/communities/{communityId}/spaces'; + static const String deleteSpace = '/communities/{communityId}/spaces/{spaceId}'; + static const String updateSpace = '/communities/{communityId}/spaces/{spaceId}'; + static const String getSpace = '/communities/{communityId}/spaces/{spaceId}'; + static const String getSpaceHierarchy = '/communities/{communityId}/spaces'; // Community Module static const String createCommunity = '/communities'; static const String getCommunityList = '/communities'; - static const String getCommunityById = - '/communities/{communityId}'; - static const String updateCommunity = - '/communities/{communityId}'; - static const String deleteCommunity = - '/communities/{communityId}'; - static const String getUserCommunities = - '/communities/user/{userUuid}'; + static const String getCommunityById = '/communities/{communityId}'; + static const String updateCommunity = '/communities/{communityId}'; + static const String deleteCommunity = '/communities/{communityId}'; + static const String getUserCommunities = '/communities/user/{userUuid}'; static const String createUserCommunity = '/communities/user'; static const String getDeviceLogsByDate = '/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'; - //product + //product static const String listProducts = '/products'; + 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'; + static const String createAutomation = '/automation'; + static const String getUnitScenes = '/communities/{communityUuid}/spaces/{spaceUuid}/scenes'; + static const String getAutomationDetails = '/automation/details/{automationId}'; + static const String getScene = '/scene/tap-to-run/{sceneId}'; + static const String deleteScene = '/scene/tap-to-run/{sceneId}'; + + static const String deleteAutomation = '/automation/{automationId}'; } diff --git a/lib/utils/constants/app_enum.dart b/lib/utils/constants/app_enum.dart index 4ca37d9b..d603c3ea 100644 --- a/lib/utils/constants/app_enum.dart +++ b/lib/utils/constants/app_enum.dart @@ -97,3 +97,17 @@ extension AccessStatusExtension on AccessStatus { enum TempModes { hot, cold, wind } enum FanSpeeds { auto, low, middle, high } + +enum AcValuesEnums { + Cooling, + Heating, + Ventilation, +} + +enum EnumEffectivePeriodOptions { + allDay, + daytime, + night, + custom, + none, +} diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 6abd689e..958c2c1c 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -237,4 +237,94 @@ class Assets { static const String delete = 'assets/icons/delete.svg'; static const String edit = 'assets/icons/edit.svg'; + //assets/icons/routine/tab_to_run.svg + static const String tabToRun = 'assets/icons/routine/tab_to_run.svg'; + + //assets/icons/routine/schedule.svg + static const String schedule = 'assets/icons/routine/schedule.svg'; + + //assets/icons/routine/map.svg + static const String map = 'assets/icons/routine/map.svg'; + + //assets/icons/routine/weather.svg + static const String weather = 'assets/icons/routine/weather.svg'; + + //assets/icons/routine/notification.svg + static const String notification = 'assets/icons/routine/notification.svg'; + + //assets/icons/routine/delay.svg + static const String delay = 'assets/icons/routine/delay.svg'; + + // Assets for functions_icons + static const String assetsSensitivityFunction = "assets/icons/functions_icons/sensitivity.svg"; + static const String assetsSensitivityOperationIcon = + "assets/icons/functions_icons/sesitivity_operation_icon.svg"; + static const String assetsAcPower = "assets/icons/functions_icons/ac_power.svg"; + static const String assetsAcPowerOFF = "assets/icons/functions_icons/ac_power_off.svg"; + static const String assetsChildLock = "assets/icons/functions_icons/child_lock.svg"; + static const String assetsFreezing = "assets/icons/functions_icons/freezing.svg"; + static const String assetsFanSpeed = "assets/icons/functions_icons/fan_speed.svg"; + static const String assetsAcCooling = "assets/icons/functions_icons/ac_cooling.svg"; + static const String assetsAcHeating = "assets/icons/functions_icons/ac_heating.svg"; + static const String assetsCelsiusDegrees = "assets/icons/functions_icons/celsius_degrees.svg"; + static const String assetsTempreture = "assets/icons/functions_icons/tempreture.svg"; + static const String assetsAcFanLow = "assets/icons/functions_icons/ac_fan_low.svg"; + static const String assetsAcFanMiddle = "assets/icons/functions_icons/ac_fan_middle.svg"; + static const String assetsAcFanHigh = "assets/icons/functions_icons/ac_fan_high.svg"; + static const String assetsAcFanAuto = "assets/icons/functions_icons/ac_fan_auto.svg"; + static const String assetsSceneChildLock = "assets/icons/functions_icons/scene_child_lock.svg"; + static const String assetsSceneChildUnlock = + "assets/icons/functions_icons/scene_child_unlock.svg"; + static const String assetsSceneRefresh = "assets/icons/functions_icons/scene_refresh.svg"; + static const String assetsLightCountdown = "assets/icons/functions_icons/light_countdown.svg"; + static const String assetsFarDetection = "assets/icons/functions_icons/far_detection.svg"; + static const String assetsFarDetectionFunction = + "assets/icons/functions_icons/far_detection_function.svg"; + static const String assetsIndicator = "assets/icons/functions_icons/indicator.svg"; + static const String assetsMotionDetection = "assets/icons/functions_icons/motion_detection.svg"; + static const String assetsMotionlessDetection = + "assets/icons/functions_icons/motionless_detection.svg"; + static const String assetsNobodyTime = "assets/icons/functions_icons/nobody_time.svg"; + static const String assetsFactoryReset = "assets/icons/functions_icons/factory_reset.svg"; + static const String assetsMasterState = "assets/icons/functions_icons/master_state.svg"; + static const String assetsSwitchAlarmSound = + "assets/icons/functions_icons/switch_alarm_sound.svg"; + static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; + +// Assets for automation_functions + static const String assetsCardUnlock = + "assets/icons/functions_icons/automation_functions/card_unlock.svg"; + static const String assetsDoorbell = + "assets/icons/functions_icons/automation_functions/doorbell.svg"; + static const String assetsDoorlockNormalOpen = + "assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg"; + static const String assetsDoubleLock = + "assets/icons/functions_icons/automation_functions/double_lock.svg"; + static const String assetsFingerprintUnlock = + "assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg"; + static const String assetsHijackAlarm = + "assets/icons/functions_icons/automation_functions/hijack_alarm.svg"; + static const String assetsLockAlarm = + "assets/icons/functions_icons/automation_functions/lock_alarm.svg"; + static const String assetsPasswordUnlock = + "assets/icons/functions_icons/automation_functions/password_unlock.svg"; + static const String assetsRemoteUnlockReq = + "assets/icons/functions_icons/automation_functions/remote_unlock_req.svg"; + static const String assetsRemoteUnlockViaApp = + "assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg"; + static const String assetsResidualElectricity = + "assets/icons/functions_icons/automation_functions/residual_electricity.svg"; + static const String assetsTempPasswordUnlock = + "assets/icons/functions_icons/automation_functions/temp_password_unlock.svg"; + static const String assetsSelfTestResult = + "assets/icons/functions_icons/automation_functions/self_test_result.svg"; + static const String assetsPresence = + "assets/icons/functions_icons/automation_functions/presence.svg"; + static const String assetsMotion = "assets/icons/functions_icons/automation_functions/motion.svg"; + static const String assetsCurrentTemp = + "assets/icons/functions_icons/automation_functions/current_temp.svg"; + static const String assetsPresenceState = + "assets/icons/functions_icons/automation_functions/presence_state.svg"; + //assets/icons/routine/automation.svg + static const String automation = 'assets/icons/routine/automation.svg'; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 2b1ce8a5..7050f13a 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -11,9 +11,13 @@ enum DeviceType { CeilingSensor, WallSensor, WH, - DoorSensor, + DS, + OneTouch, + TowTouch, + ThreeTouch, GarageDoor, WaterLeak, + DoorSensor, Other, } /* diff --git a/lib/utils/extension/build_context_x.dart b/lib/utils/extension/build_context_x.dart index dbdbb347..0abd16a1 100644 --- a/lib/utils/extension/build_context_x.dart +++ b/lib/utils/extension/build_context_x.dart @@ -23,6 +23,11 @@ extension BuildContextExt on BuildContext { VoidCallback? onDismiss, bool? hideConfirmButton, final double? dialogWidth, + TextStyle? titleStyle, + String? onDismissText, + String? onConfirmText, + Color? onDismissColor, + Color? onConfirmColor, }) { showDialog( context: this, @@ -42,10 +47,11 @@ extension BuildContextExt on BuildContext { /// header widget Text( title, - style: context.textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), + style: titleStyle ?? + context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), ), Padding( padding: const EdgeInsets.symmetric( @@ -79,9 +85,10 @@ extension BuildContextExt on BuildContext { }, child: Center( child: Text( - 'Cancel', - style: context.textTheme.bodyMedium! - .copyWith(color: ColorsManager.greyColor), + onDismissText ?? 'Cancel', + style: context.textTheme.bodyMedium!.copyWith( + color: onDismissColor ?? + ColorsManager.greyColor), ), ), ), @@ -94,9 +101,9 @@ extension BuildContextExt on BuildContext { onTap: onConfirm, child: Center( child: Text( - 'Confirm', + onConfirmText ?? 'Confirm', style: context.textTheme.bodyMedium!.copyWith( - color: + color: onConfirmColor ?? ColorsManager.primaryColorWithOpacity), ), ), diff --git a/lib/utils/style.dart b/lib/utils/style.dart index 12b72521..a80c68d6 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; + import 'color_manager.dart'; -InputDecoration? textBoxDecoration({bool suffixIcon = false}) => - InputDecoration( +InputDecoration? textBoxDecoration({bool suffixIcon = false}) => InputDecoration( focusColor: ColorsManager.grayColor, suffixIcon: suffixIcon ? const Icon(Icons.search) : null, hintText: 'Search', @@ -30,17 +30,23 @@ InputDecoration? textBoxDecoration({bool suffixIcon = false}) => ), ); -BoxDecoration containerDecoration = BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(0, 5), // changes position of shadow - ), - ], - color: ColorsManager.boxColor, - borderRadius: const BorderRadius.all(Radius.circular(10))); +BoxDecoration containerDecoration = BoxDecoration(boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow + ), +], color: ColorsManager.boxColor, borderRadius: const BorderRadius.all(Radius.circular(10))); + +BoxDecoration containerWhiteDecoration = BoxDecoration(boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow + ), +], color: ColorsManager.whiteColors, borderRadius: const BorderRadius.all(Radius.circular(15))); BoxDecoration subSectionContainerDecoration = BoxDecoration( color: ColorsManager.whiteColors, diff --git a/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 b/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 new file mode 100644 index 00000000..309af193 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 @@ -0,0 +1,95 @@ + + + + + logFormatVersion + 11 + logs + + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + S + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 0 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 0 + + schemeIdentifier-containerName + Runner project + schemeIdentifier-schemeName + Flutter Assemble + schemeIdentifier-sharedScheme + 1 + signature + Cleaning workspace Runner with scheme Flutter Assemble + timeStartedRecording + 752000674.27645695 + timeStoppedRecording + 752000674.42918503 + title + Cleaning workspace Runner with scheme Flutter Assemble + uniqueIdentifier + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + FB42CDDD-C79D-4D4B-891A-12C476DFCB10 + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + S + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 0 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 0 + + schemeIdentifier-containerName + Runner project + schemeIdentifier-schemeName + Runner + schemeIdentifier-sharedScheme + 1 + signature + Cleaning workspace Runner with scheme Runner + timeStartedRecording + 752000674.90370798 + timeStoppedRecording + 752000675.05962098 + title + Cleaning workspace Runner with scheme Runner + uniqueIdentifier + FB42CDDD-C79D-4D4B-891A-12C476DFCB10 + + + + diff --git a/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog b/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog new file mode 100644 index 00000000..c811e6cb Binary files /dev/null and b/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog differ diff --git a/macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog b/macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog new file mode 100644 index 00000000..b9ccd504 Binary files /dev/null and b/macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog differ diff --git a/macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist new file mode 100644 index 00000000..0c8e2d35 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist @@ -0,0 +1,53 @@ + + + + + logFormatVersion + 11 + logs + + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + S + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 0 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 0 + + schemeIdentifier-containerName + Runner project + schemeIdentifier-schemeName + Flutter Assemble + schemeIdentifier-sharedScheme + 1 + signature + Cleaning workspace Runner with scheme Flutter Assemble + timeStartedRecording + 752000674.27645695 + timeStoppedRecording + 752000674.42918503 + title + Cleaning workspace Runner with scheme Flutter Assemble + uniqueIdentifier + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + + + diff --git a/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/info.plist b/macos/DerivedData/Runner/info.plist new file mode 100644 index 00000000..3594c152 --- /dev/null +++ b/macos/DerivedData/Runner/info.plist @@ -0,0 +1,10 @@ + + + + + LastAccessedDate + 2024-10-30T17:04:35Z + WorkspacePath + /Users/akmz/Developer/web/syncrow-web/web/macos/Runner.xcworkspace + + diff --git a/pubspec.lock b/pubspec.lock index 56d85c9a..192106d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -597,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + time_picker_spinner: + dependency: "direct main" + description: + name: time_picker_spinner + sha256: "53d824801d108890d22756501e7ade9db48b53dac1ec41580499dd4ebd128e3c" + url: "https://pub.dev" + source: hosted + version: "1.0.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 155381ae..ffff62ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: flutter_dotenv: ^5.1.0 fl_chart: ^0.69.0 uuid: ^4.4.2 - + time_picker_spinner: ^1.0.0 dev_dependencies: flutter_test: @@ -78,6 +78,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/icons/automation_functions/ + - assets/icons/functions_icons/ + - assets/icons/routine/ - assets/icons/ - assets/images/ - assets/