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..ffc57131 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -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) => DeviceManagementBloc()..add(FetchDevices()), + ), + BlocProvider( + create: (context) => + SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), + ), + ], child: WebScaffold( appBarTitle: FittedBox( child: Text( @@ -20,26 +33,87 @@ 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 || + deviceState is DeviceManagementFiltered) { + final devices = (deviceState as dynamic).devices ?? + (deviceState as DeviceManagementFiltered).filteredDevices; + + return DeviceManagementBody(devices: devices); + } else { + return const Center(child: Text('Error fetching Devices')); + } + }, + ); + }), ), ); } } - - - diff --git a/lib/pages/device_managment/all_devices/widgets/device_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 71974156..e3bec220 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -1,10 +1,9 @@ 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'; -import 'package:syncrow_web/utils/style.dart'; class DeviceSearchFilters extends StatefulWidget { const DeviceSearchFilters({super.key}); @@ -77,6 +76,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..05f63123 --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart @@ -0,0 +1,125 @@ +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; + } + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent( + // EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); + + 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)); + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent(EffectiveTime( + // start: state.customStartTime ?? '00:00', + // end: state.customEndTime ?? '23:59', + // loops: 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)); + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent( + // EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); + } + + void _onResetEffectivePeriod(ResetEffectivePeriod event, Emitter emit) { + emit(state.copyWith( + selectedPeriod: EnumEffectivePeriodOptions.allDay, + customStartTime: '00:00', + customEndTime: '23:59', + selectedDaysBinary: '1111111')); + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent(EffectiveTime(start: '00:00', end: '23:59', loops: '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..88414196 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; +import 'package:syncrow_web/services/routines_api.dart'; + +part 'routine_event.dart'; +part 'routine_state.dart'; + +const spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6'; + +class RoutineBloc extends Bloc { + bool isAutomation = false; + bool isTabToRun = false; + + RoutineBloc() : super(const RoutineState()) { + on(_onAddToIfContainer); + on(_onAddToThenContainer); + on(_onLoadScenes); + on(_onLoadAutomation); + on(_onAddFunctionsToRoutine); + on(_onSearchRoutines); + on(_onAddSelectedIcon); + on(_onCreateScene); + on(_onRemoveDragCard); + // on(_onRemoveFunction); + // on(_onClearFunctions); + } + + void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { + final updatedIfItems = List>.from(state.ifItems)..add(event.item); + if (event.isTabToRun) { + isTabToRun = true; + isAutomation = false; + } else { + isTabToRun = false; + isAutomation = true; + } + emit(state.copyWith(ifItems: updatedIfItems)); + } + + void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + final updatedThenItems = List>.from(state.thenItems)..add(event.item); + emit(state.copyWith(thenItems: updatedThenItems)); + } + + void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { + try { + if (event.functions.isEmpty) return; + + final currentSelectedFunctions = + Map>.from(state.selectedFunctions); + + if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { + currentSelectedFunctions[event.uniqueCustomId] = + List.from(currentSelectedFunctions[event.uniqueCustomId]!)..addAll(event.functions); + } 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); + 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); + emit(state.copyWith( + automations: automations, + isLoading: false, + )); + } 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(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 + .map((item) { + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + if (functions.isEmpty) return null; + + final function = functions.first; + if (item['deviceId'] == 'delay') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'delay', + executorProperty: CreateSceneExecutorProperty( + functionCode: '', + functionValue: '', + delaySeconds: function.value, + ), + ); + } + + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + executorProperty: CreateSceneExecutorProperty( + functionCode: function.functionCode.toString(), + functionValue: function.value, + delaySeconds: 0, + ), + ); + }) + .whereType() + .toList(); + + final createSceneModel = CreateSceneModel( + spaceUuid: spaceId, + iconId: state.selectedIcon ?? '', + showInDevice: true, + sceneName: state.routineName ?? '', + decisionExpr: 'and', + actions: actions, + ); + + final result = await SceneApi.createScene(createSceneModel); + if (result['success']) { + emit(const RoutineState()); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: result['message'], + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: e.toString(), + )); + } + } + + FutureOr _onRemoveDragCard(RemoveDragCard event, Emitter emit) { + if (event.isFromThen) { + /// remove element from thenItems at specific index + final thenItems = List>.from(state.thenItems); + thenItems.removeAt(event.index); + emit(state.copyWith(thenItems: thenItems)); + } else { + final ifItems = List>.from(state.ifItems); + ifItems.removeAt(event.index); + emit(state.copyWith(ifItems: ifItems)); + } + } +} 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..9e154cf4 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -0,0 +1,90 @@ +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; + + const LoadScenes(this.unitId); + + @override + List get props => [unitId]; +} + +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; + const RemoveDragCard({required this.index, required this.isFromThen}); + @override + List get props => [index]; +} + +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..f057c2c0 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -0,0 +1,76 @@ +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; + + 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}); + + 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}) { + 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); + } + + @override + List get props => [ + ifItems, + thenItems, + scenes, + automations, + selectedFunctions, + isLoading, + errorMessage, + routineName, + selectedIcon, + loadScenesErrorMessage, + loadAutomationErrorMessage, + searchText + ]; +} 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..33cfffee --- /dev/null +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -0,0 +1,68 @@ +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, + ) async { + final functions = data['functions'] as List; + + try { + final result = await _getDialogForDeviceType( + context, + data['productType'], + data, + functions, + ); + + if (result != null) { + return result; + } + } catch (e) { + debugPrint('Error: $e'); + } + + return null; + } + + static Future?> _getDialogForDeviceType( + BuildContext context, + String productType, + Map data, + List functions, + ) 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']); + + case '1G': + return OneGangSwitchHelper.showSwitchFunctionsDialog(context, functions, + data['device'], deviceSelectedFunctions, data['uniqueCustomId']); + case '2G': + return TwoGangSwitchHelper.showSwitchFunctionsDialog(context, functions, + data['device'], deviceSelectedFunctions, data['uniqueCustomId']); + case '3G': + return ThreeGangSwitchHelper.showSwitchFunctionsDialog( + context, + functions, + data['device'], + deviceSelectedFunctions, + data['uniqueCustomId'], + ); + 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..69329992 --- /dev/null +++ b/lib/pages/routiens/helper/save_routine_helper.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SaveRoutineHelper { + static Future showSaveRoutineDialog(BuildContext context) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: 600, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader('Create a scene: ${state.routineName ?? ""}'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left side - IF + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'IF:', + style: TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 8), + if (context.read().isTabToRun) + ListTile( + leading: SvgPicture.asset( + Assets.tabToRun, + width: 24, + height: 24, + ), + title: const Text('Tab to run'), + ), + ], + ), + ), + const SizedBox(width: 16), + // Right side - THEN items + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'THEN:', + style: TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 8), + ...state.thenItems.map((item) { + final functions = state.selectedFunctions[ + item['uniqueCustomId']] ?? + []; + return ListTile( + leading: SvgPicture.asset( + item['imagePath'], + width: 22, + height: 22, + ), + title: Text(item['title'], + style: const TextStyle(fontSize: 14)), + subtitle: Wrap( + children: functions + .map((f) => Text( + '${f.operationName}: ${f.value}, ', + style: const TextStyle( + color: + ColorsManager.grayColor, + fontSize: 8), + overflow: TextOverflow.ellipsis, + maxLines: 3, + )) + .toList(), + ), + ); + }), + ], + ), + ), + ], + ), + ), + if (state.errorMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + state.errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ), + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: () { + context + .read() + .add(const CreateSceneEvent()); + Navigator.pop(context); + }, + isConfirmEnabled: true, + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/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/create_scene_model.dart b/lib/pages/routiens/models/create_scene/create_scene_model.dart new file mode 100644 index 00000000..c669aa9a --- /dev/null +++ b/lib/pages/routiens/models/create_scene/create_scene_model.dart @@ -0,0 +1,230 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +class CreateSceneModel { + String spaceUuid; + String iconId; + bool showInDevice; + String sceneName; + String decisionExpr; + List actions; + + CreateSceneModel({ + required this.spaceUuid, + required this.iconId, + required this.showInDevice, + required this.sceneName, + required this.decisionExpr, + required this.actions, + }); + + CreateSceneModel copyWith({ + String? spaceUuid, + String? iconId, + bool? showInDevice, + String? sceneName, + String? decisionExpr, + List? actions, + bool? showInHomePage, + }) { + return CreateSceneModel( + spaceUuid: spaceUuid ?? this.spaceUuid, + iconId: iconId ?? this.iconId, + showInDevice: showInDevice ?? this.showInDevice, + sceneName: sceneName ?? this.sceneName, + decisionExpr: decisionExpr ?? this.decisionExpr, + actions: actions ?? this.actions, + ); + } + + Map toMap([String? sceneId]) { + return { + if (sceneId == null) 'spaceUuid': spaceUuid, + if (iconId.isNotEmpty) 'iconUuid': iconId, + 'showInHomePage': showInDevice, + 'sceneName': sceneName, + 'decisionExpr': decisionExpr, + 'actions': actions.map((x) => x.toMap()).toList(), + }; + } + + factory CreateSceneModel.fromMap(Map map) { + return CreateSceneModel( + spaceUuid: map['spaceUuid'] ?? '', + showInDevice: map['showInHomePage'] ?? false, + iconId: map['iconUuid'] ?? '', + sceneName: map['sceneName'] ?? '', + decisionExpr: map['decisionExpr'] ?? '', + actions: List.from( + map['actions']?.map((x) => CreateSceneAction.fromMap(x))), + ); + } + + String toJson([String? sceneId]) => json.encode(toMap(sceneId)); + + factory CreateSceneModel.fromJson(String source) => + CreateSceneModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'CreateSceneModel(unitUuid: $spaceUuid, sceneName: $sceneName, decisionExpr: $decisionExpr, actions: $actions)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneModel && + other.spaceUuid == spaceUuid && + other.iconId == iconId && + other.showInDevice == showInDevice && + other.sceneName == sceneName && + other.decisionExpr == decisionExpr && + listEquals(other.actions, actions); + } + + @override + int get hashCode { + return spaceUuid.hashCode ^ + sceneName.hashCode ^ + decisionExpr.hashCode ^ + actions.hashCode; + } +} + +class CreateSceneAction { + String entityId; + String actionExecutor; + CreateSceneExecutorProperty? executorProperty; + + CreateSceneAction({ + required this.entityId, + required this.actionExecutor, + required this.executorProperty, + }); + + CreateSceneAction copyWith({ + String? entityId, + String? actionExecutor, + CreateSceneExecutorProperty? executorProperty, + }) { + return CreateSceneAction( + entityId: entityId ?? this.entityId, + actionExecutor: actionExecutor ?? this.actionExecutor, + executorProperty: executorProperty ?? this.executorProperty, + ); + } + + Map toMap() { + if (executorProperty != null) { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'executorProperty': executorProperty?.toMap(actionExecutor), + }; + } else { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + }; + } + } + + factory CreateSceneAction.fromMap(Map map) { + return CreateSceneAction( + entityId: map['entityId'] ?? '', + actionExecutor: map['actionExecutor'] ?? '', + executorProperty: + CreateSceneExecutorProperty.fromMap(map['executorProperty']), + ); + } + + String toJson() => json.encode(toMap()); + + factory CreateSceneAction.fromJson(String source) => + CreateSceneAction.fromMap(json.decode(source)); + + @override + String toString() => + 'CreateSceneAction(entityId: $entityId, actionExecutor: $actionExecutor, executorProperty: $executorProperty)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneAction && + other.entityId == entityId && + other.actionExecutor == actionExecutor && + other.executorProperty == executorProperty; + } + + @override + int get hashCode => + entityId.hashCode ^ actionExecutor.hashCode ^ executorProperty.hashCode; +} + +class CreateSceneExecutorProperty { + String functionCode; + dynamic functionValue; + int delaySeconds; + + CreateSceneExecutorProperty({ + required this.functionCode, + required this.functionValue, + required this.delaySeconds, + }); + + CreateSceneExecutorProperty copyWith({ + String? functionCode, + dynamic functionValue, + int? delaySeconds, + }) { + return CreateSceneExecutorProperty( + functionCode: functionCode ?? this.functionCode, + functionValue: functionValue ?? this.functionValue, + delaySeconds: delaySeconds ?? this.delaySeconds, + ); + } + + Map toMap(String actionExecutor) { + final map = {}; + if (functionCode.isNotEmpty) map['functionCode'] = functionCode; + if (functionValue != null) map['functionValue'] = functionValue; + if (actionExecutor == 'delay' && delaySeconds > 0) { + map['delaySeconds'] = delaySeconds; + } + return map; + } + + factory CreateSceneExecutorProperty.fromMap(Map map) { + return CreateSceneExecutorProperty( + functionCode: map['functionCode'] ?? '', + functionValue: map['functionValue'] ?? '', + delaySeconds: map['delaySeconds']?.toInt() ?? 0, + ); + } + + String toJson(String actionExecutor) => json.encode(toMap(actionExecutor)); + + factory CreateSceneExecutorProperty.fromJson(String source) => + CreateSceneExecutorProperty.fromMap(json.decode(source)); + + @override + String toString() => + 'CreateSceneExecutorProperty(functionCode: $functionCode, functionValue: $functionValue, delaySeconds: $delaySeconds)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneExecutorProperty && + other.functionCode == functionCode && + other.functionValue == functionValue && + other.delaySeconds == delaySeconds; + } + + @override + int get hashCode => + functionCode.hashCode ^ functionValue.hashCode ^ delaySeconds.hashCode; +} diff --git a/lib/pages/routiens/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_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..e2075579 --- /dev/null +++ b/lib/pages/routiens/models/routine_model.dart @@ -0,0 +1,36 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ScenesModel { + final String id; + final String name; + final String status; + final String type; + final String? icon; + + ScenesModel({ + required this.id, + required this.name, + required this.status, + required this.type, + this.icon, + }); + + factory ScenesModel.fromJson(Map json, + {bool? isAutomation}) => + ScenesModel( + id: json["id"], + name: json["name"] ?? '', + status: json["status"] ?? '', + type: json["type"] ?? '', + icon: (isAutomation ?? false) + ? Assets.automation + : json["icon"] as String?, + ); + + Map toJson() => { + "id": id, + "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..38750991 --- /dev/null +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -0,0 +1,89 @@ +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 { + const CreateNewRoutineView({super.key}); + + @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..d9d9bc2f --- /dev/null +++ b/lib/pages/routiens/view/effective_period_view.dart @@ -0,0 +1,50 @@ +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/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 BlocProvider( + create: (_) => EffectPeriodBloc(), + child: 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..27690a9c --- /dev/null +++ b/lib/pages/routiens/view/routines_view.dart @@ -0,0 +1,75 @@ +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/view/create_new_routine_view.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.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ), + ), + SizedBox( + height: 200, + width: 150, + child: GestureDetector( + onTap: () { + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(true), + ); + }, + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + color: ColorsManager.whiteColors, + child: Center( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(120), + border: Border.all( + color: ColorsManager.greyColor, + width: 2.0, + ), + ), + height: 70, + width: 70, + child: Icon( + Icons.add, + color: ColorsManager.dialogBlueTitle, + size: 40, + ), + ), + ), + ), + ), + ), + const Spacer(), + ], + ), + ); + }, + ); + } +} 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..90c6baec --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_footer.dart @@ -0,0 +1,76 @@ +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; + + const DialogFooter({ + Key? key, + required this.onCancel, + required this.onConfirm, + required this.isConfirmEnabled, + }) : 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: [ + _buildFooterButton( + context, + 'Cancel', + onCancel, + width: isConfirmEnabled ? 299 : 179, + ), + if (isConfirmEnabled) + Row( + children: [ + Container(width: 1, height: 50, color: ColorsManager.greyColor), + _buildFooterButton( + context, + 'Confirm', + onConfirm, + width: 299, + ), + ], + ), + ], + ), + ); + } + + Widget _buildFooterButton( + BuildContext context, + String text, + VoidCallback? onTap, { + required double width, + }) { + return GestureDetector( + onTap: onTap, + child: SizedBox( + height: 50, + width: width, + 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..db8ad1c9 --- /dev/null +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -0,0 +1,178 @@ +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.network(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..cfb1b27c --- /dev/null +++ b/lib/pages/routiens/widgets/if_container.dart @@ -0,0 +1,121 @@ +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/constants/assets.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: [ + const Text('IF', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + if (context.read().isTabToRun) + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + deviceData: {}, + ), + ], + ), + if (!context.read().isTabToRun) + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + state.ifItems.length, + (index) => 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)); + }, + )), + ), + ], + ), + ); + }, + onWillAccept: (data) => data != null, + onAccept: (data) async { + // final uniqueCustomId = const Uuid().v4(); + + // final mutableData = Map.from(data); + // mutableData['uniqueCustomId'] = uniqueCustomId; + + // if (!context.read().isTabToRun) { + // if (data['deviceId'] == 'tab_to_run') { + // context.read().add(AddToIfContainer(data, true)); + // } else { + // final result = + // await DeviceDialogHelper.showDeviceDialog(context, mutableData); + // if (result != null) { + // context + // .read() + // .add(AddToIfContainer(mutableData, false)); + // } else if (!['AC', '1G', '2G', '3G'] + // .contains(data['productType'])) { + // context + // .read() + // .add(AddToIfContainer(mutableData, false)); + // } + // } + //} + final uniqueCustomId = const Uuid().v4(); + + final mutableData = Map.from(data); + mutableData['uniqueCustomId'] = uniqueCustomId; + + if (!context.read().isTabToRun) { + if (mutableData['deviceId'] == 'tab_to_run') { + context + .read() + .add(AddToIfContainer(mutableData, true)); + } else { + final result = await DeviceDialogHelper.showDeviceDialog( + context, mutableData); + + if (result != null) { + context + .read() + .add(AddToIfContainer(mutableData, false)); + } else if (!['AC', '1G', '2G', '3G'] + .contains(mutableData['productType'])) { + context + .read() + .add(AddToIfContainer(mutableData, false)); + } + } + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/period_option.dart b/lib/pages/routiens/widgets/period_option.dart new file mode 100644 index 00000000..8a89e2f9 --- /dev/null +++ b/lib/pages/routiens/widgets/period_option.dart @@ -0,0 +1,103 @@ +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)), + 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..e8b87a16 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart @@ -0,0 +1,415 @@ +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, + ) 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 ?? '', + ), + ), + ], + ), + ), + 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, + }) { + 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, + ); + } + + 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, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _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/delay_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart new file mode 100644 index 00000000..1191ac58 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart @@ -0,0 +1,74 @@ +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, String uniqueCustomId) async { + int hours = 0; + int minutes = 0; + + return showDialog?>( + context: context, + builder: (BuildContext context) { + 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, + ) + ], + uniqueCustomId, + )); + + Navigator.pop(context, { + 'deviceId': 'delay', + 'value': totalSeconds, + }); + }, + isConfirmEnabled: 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..00f936d6 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart @@ -0,0 +1,391 @@ +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, + ) 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 ?? '', + ), + ), + ], + ), + ), + 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, + }) { + 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, + ); + } + + 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, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _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..621a5219 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart @@ -0,0 +1,465 @@ +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/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/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().isAutomation; + return BlocProvider( + create: (_) => + SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + String selectedIcon = ''; + List list = []; + if (state is TabToRunSettingLoaded) { + selectedIcon = state.selectedIcon; + list = state.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< + SettingBloc>() + .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< + SettingBloc>() + .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: state 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: () { + 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..ae704ded --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart @@ -0,0 +1,393 @@ +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, + ) 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 ?? '', + ), + ), + ], + ), + ), + 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, + }) { + 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, + ); + } + + 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, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _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..d017103e --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart @@ -0,0 +1,392 @@ +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, + ) 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 ?? '', + ), + ), + ], + ), + ), + 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, + }) { + 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, + ); + } + + 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, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _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..01352bf8 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -0,0 +1,215 @@ +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/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/save_routine_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/setting_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RoutineSearchAndButtons extends StatelessWidget { + const RoutineSearchAndButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + 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', + height: 40, + controller: TextEditingController(), + hintText: 'Please enter the name', + boxDecoration: containerWhiteDecoration, + elevation: 0, + borderRadius: 15, + isRequired: true, + width: 450, + onChanged: (value) { + // context.read().add(SearchRoutines(value)); + }, + ), + ), + (constraints.maxWidth <= 1000) + ? const SizedBox() + : SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () async { + final result = + await SettingHelper.showSettingDialog( + context: context, + ); + if (result != null) { + context + .read() + .add(AddSelectedIcon(result)); + } + }, + 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: () {}, + 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: () { + SaveRoutineHelper.showSaveRoutineDialog(context); + }, + 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: () {}, + 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: () {}, + 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: () {}, + 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..7a26979c --- /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)) + ..add(const LoadAutomation(spaceId)); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { + 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: Assets.logo, + 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..2bc65b0e --- /dev/null +++ b/lib/pages/routiens/widgets/then_container.dart @@ -0,0 +1,92 @@ +// 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/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) => 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)); + }, + ))), + ], + ), + ), + ); + }, + onWillAccept: (data) => data != null, + onAccept: (data) async { + final uniqueCustomId = const Uuid().v4(); + + final mutableData = Map.from(data); + mutableData['uniqueCustomId'] = uniqueCustomId; + + if (mutableData['deviceId'] == 'delay') { + final result = await DelayHelper.showDelayPickerDialog( + context, mutableData['uniqueCustomId']); + + if (result != null) { + context.read().add(AddToThenContainer({ + ...mutableData, + 'imagePath': Assets.delay, + 'title': 'Delay', + })); + } + return; + } + + final result = + await DeviceDialogHelper.showDeviceDialog(context, mutableData); + + 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/services/routines_api.dart b/lib/services/routines_api.dart new file mode 100644 index 00000000..0ce106b2 --- /dev/null +++ b/lib/services/routines_api.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_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 { +// final response = await _httpService.post( +// path: ApiEndpoints.createAutomation, +// body: createAutomationModel.toMap(), +// showServerMessage: false, +// expectedResponseModel: (json) { +// return json; +// }, +// ); +// return response; +// } catch (e) { +// 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 scene by unit id + + static Future> getScenesByUnitId(String unitId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getSpaceScenes.replaceAll('{unitUuid}', unitId), + showServerMessage: false, + expectedResponseModel: (json) { + List scenes = []; + for (var scene in json) { + scenes.add(ScenesModel.fromJson(scene)); + } + 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) => SceneDetailsModel.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) => SceneDetailsModel.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 481fb2fa..140ed370 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -41,6 +41,6 @@ abstract class ColorsManager { static const Color blue4 = Color(0xFF001E7E); static const Color textGreen = Color(0xFF008905); static const Color yaGreen = Color(0xFFFFBF44); + 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 bf167ab5..a5decc3b 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -50,4 +50,8 @@ abstract class ApiEndpoints { static const String factoryReset = '/device/factory/reset/{deviceUuid}'; static const String powerClamp = '/device/{powerClampUuid}/power-clamp/status'; + static const String getSpaceScenes = '/scene/tap-to-run/{unitUuid}'; + static const String getSpaceAutomation = '/automation/{unitUuid}'; + static const String getIconScene = '/scene/icon'; + static const String createScene = '/scene/tap-to-run'; } 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 dad9b261..cffaf472 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -13,10 +13,12 @@ class Assets { static const String rightLine = "assets/images/right_line.png"; static const String google = "assets/images/google.svg"; static const String facebook = "assets/images/facebook.svg"; - static const String invisiblePassword = "assets/images/Password_invisible.svg"; + static const String invisiblePassword = + "assets/images/Password_invisible.svg"; static const String visiblePassword = "assets/images/password_visible.svg"; static const String accessIcon = "assets/images/access_icon.svg"; - static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; + static const String spaseManagementIcon = + "assets/images/spase_management_icon.svg"; static const String devicesIcon = "assets/images/devices_icon.svg"; static const String moveinIcon = "assets/images/movein_icon.svg"; static const String constructionIcon = "assets/images/construction_icon.svg"; @@ -29,13 +31,15 @@ class Assets { static const String emptyTable = "assets/images/empty_table.svg"; // General assets - static const String motionlessDetection = "assets/icons/motionless_detection.svg"; + static const String motionlessDetection = + "assets/icons/motionless_detection.svg"; static const String acHeating = "assets/icons/ac_heating.svg"; static const String acPowerOff = "assets/icons/ac_power_off.svg"; static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; static const String switchAlarmSound = "assets/icons/switch_alarm_sound.svg"; static const String resetOff = "assets/icons/reset_off.svg"; - static const String sensitivityOperationIcon = "assets/icons/sesitivity_operation_icon.svg"; + static const String sensitivityOperationIcon = + "assets/icons/sesitivity_operation_icon.svg"; static const String motionDetection = "assets/icons/motion_detection.svg"; static const String freezing = "assets/icons/freezing.svg"; static const String indicator = "assets/icons/indicator.svg"; @@ -56,35 +60,56 @@ class Assets { static const String celsiusDegrees = "assets/icons/celsius_degrees.svg"; static const String masterState = "assets/icons/master_state.svg"; static const String acPower = "assets/icons/ac_power.svg"; - static const String farDetectionFunction = "assets/icons/far_detection_function.svg"; + static const String farDetectionFunction = + "assets/icons/far_detection_function.svg"; static const String nobodyTime = "assets/icons/nobody_time.svg"; // Automation functions - static const String tempPasswordUnlock = "assets/icons/automation_functions/temp_password_unlock.svg"; - static const String doorlockNormalOpen = "assets/icons/automation_functions/doorlock_normal_open.svg"; - static const String doorbell = "assets/icons/automation_functions/doorbell.svg"; - static const String remoteUnlockViaApp = "assets/icons/automation_functions/remote_unlock_via_app.svg"; - static const String doubleLock = "assets/icons/automation_functions/double_lock.svg"; - static const String selfTestResult = "assets/icons/automation_functions/self_test_result.svg"; - static const String lockAlarm = "assets/icons/automation_functions/lock_alarm.svg"; - static const String presenceState = "assets/icons/automation_functions/presence_state.svg"; - static const String currentTemp = "assets/icons/automation_functions/current_temp.svg"; - static const String presence = "assets/icons/automation_functions/presence.svg"; - static const String residualElectricity = "assets/icons/automation_functions/residual_electricity.svg"; - static const String hijackAlarm = "assets/icons/automation_functions/hijack_alarm.svg"; - static const String passwordUnlock = "assets/icons/automation_functions/password_unlock.svg"; - static const String remoteUnlockRequest = "assets/icons/automation_functions/remote_unlock_req.svg"; - static const String cardUnlock = "assets/icons/automation_functions/card_unlock.svg"; + static const String tempPasswordUnlock = + "assets/icons/automation_functions/temp_password_unlock.svg"; + static const String doorlockNormalOpen = + "assets/icons/automation_functions/doorlock_normal_open.svg"; + static const String doorbell = + "assets/icons/automation_functions/doorbell.svg"; + static const String remoteUnlockViaApp = + "assets/icons/automation_functions/remote_unlock_via_app.svg"; + static const String doubleLock = + "assets/icons/automation_functions/double_lock.svg"; + static const String selfTestResult = + "assets/icons/automation_functions/self_test_result.svg"; + static const String lockAlarm = + "assets/icons/automation_functions/lock_alarm.svg"; + static const String presenceState = + "assets/icons/automation_functions/presence_state.svg"; + static const String currentTemp = + "assets/icons/automation_functions/current_temp.svg"; + static const String presence = + "assets/icons/automation_functions/presence.svg"; + static const String residualElectricity = + "assets/icons/automation_functions/residual_electricity.svg"; + static const String hijackAlarm = + "assets/icons/automation_functions/hijack_alarm.svg"; + static const String passwordUnlock = + "assets/icons/automation_functions/password_unlock.svg"; + static const String remoteUnlockRequest = + "assets/icons/automation_functions/remote_unlock_req.svg"; + static const String cardUnlock = + "assets/icons/automation_functions/card_unlock.svg"; static const String motion = "assets/icons/automation_functions/motion.svg"; - static const String fingerprintUnlock = "assets/icons/automation_functions/fingerprint_unlock.svg"; + static const String fingerprintUnlock = + "assets/icons/automation_functions/fingerprint_unlock.svg"; // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; - static const String sensorPresenceIcon = "assets/icons/sensor_presence_ic.svg"; + static const String sensorPresenceIcon = + "assets/icons/sensor_presence_ic.svg"; static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; - static const String illuminanceRecordIcon = "assets/icons/illuminance_record_ic.svg"; - static const String presenceRecordIcon = "assets/icons/presence_record_ic.svg"; - static const String helpDescriptionIcon = "assets/icons/help_description_ic.svg"; + static const String illuminanceRecordIcon = + "assets/icons/illuminance_record_ic.svg"; + static const String presenceRecordIcon = + "assets/icons/presence_record_ic.svg"; + static const String helpDescriptionIcon = + "assets/icons/help_description_ic.svg"; static const String lightPulp = "assets/icons/light_pulb.svg"; static const String acDevice = "assets/icons/ac_device.svg"; @@ -158,7 +183,8 @@ class Assets { //assets/icons/water_leak_normal.svg static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; //assets/icons/water_leak_detected.svg - static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; + static const String waterLeakDetected = + 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -192,4 +218,120 @@ class Assets { //assets/icons/sos_normal.svg static const String sosNormal = 'assets/icons/sos_normal.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/style.dart b/lib/utils/style.dart index 145cf8a1..04108333 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,14 +30,20 @@ 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))); 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 92a76b10..cfd8b310 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -137,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" fl_chart: dependency: "direct main" description: @@ -533,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -573,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: @@ -581,6 +613,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7742e4da..3aa81860 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,8 @@ dependencies: dropdown_search: ^5.0.6 flutter_dotenv: ^5.1.0 fl_chart: ^0.69.0 + time_picker_spinner: ^1.0.0 + uuid: ^4.4.0 dev_dependencies: flutter_test: @@ -76,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/