From 584845ffdc380d3af5e0182a49c2be318d05188d Mon Sep 17 00:00:00 2001 From: Rafeek Alkhoudare Date: Thu, 22 May 2025 04:52:23 -0500 Subject: [PATCH 001/105] fix horizontal scroll bar --- lib/pages/common/custom_table.dart | 36 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 60abc0d2..ac96bce8 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -63,7 +63,8 @@ class _DynamicTableState extends State { } } - bool _compareListOfLists(List> oldList, List> newList) { + bool _compareListOfLists( + List> oldList, List> newList) { // Check if the old and new lists are the same if (oldList.length != newList.length) return false; @@ -111,8 +112,8 @@ class _DynamicTableState extends State { trackVisibility: true, child: Scrollbar( controller: _horizontalScrollController, - thumbVisibility: false, - trackVisibility: false, + thumbVisibility: true, + trackVisibility: true, notificationPredicate: (notif) => notif.depth == 1, child: SingleChildScrollView( controller: _verticalScrollController, @@ -132,7 +133,8 @@ class _DynamicTableState extends State { children: [ if (widget.withCheckBox) _buildSelectAllCheckbox(), ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell(widget.headers[index], index); + return _buildTableHeaderCell( + widget.headers[index], index); }) //...widget.headers.map((header) => _buildTableHeaderCell(header)), ], @@ -153,11 +155,14 @@ class _DynamicTableState extends State { height: 15, ), Text( - widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices', + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', style: Theme.of(context) .textTheme .bodySmall! - .copyWith(color: ColorsManager.grayColor), + .copyWith( + color: ColorsManager.grayColor), ) ], ), @@ -166,12 +171,17 @@ class _DynamicTableState extends State { ], ) : Column( - children: List.generate(widget.data.length, (index) { + children: + List.generate(widget.data.length, (index) { final row = widget.data[index]; return Row( children: [ - if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08), - ...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)), + if (widget.withCheckBox) + _buildRowCheckbox( + index, widget.size.height * 0.08), + ...row.map((cell) => _buildTableCell( + cell.toString(), + widget.size.height * 0.08)), ], ); }), @@ -196,7 +206,9 @@ class _DynamicTableState extends State { ), child: Checkbox( value: _selectAll, - onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, + onChanged: widget.withSelectAll && widget.data.isNotEmpty + ? _toggleSelectAll + : null, ), ); } @@ -238,7 +250,9 @@ class _DynamicTableState extends State { constraints: const BoxConstraints.expand(height: 40), alignment: Alignment.centerLeft, child: Padding( - padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), + padding: EdgeInsets.symmetric( + horizontal: index == widget.headers.length - 1 ? 12 : 8.0, + vertical: 4), child: Text( title, style: context.textTheme.titleSmall!.copyWith( From 8caee328229308ca5e9664fac66b247d348f8068 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 18 Jun 2025 09:39:49 +0300 Subject: [PATCH 002/105] Initialized new `SpaceManagementPage`. --- .../views/space_management_page.dart | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/views/space_management_page.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart new file mode 100644 index 00000000..03e17165 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class SpaceManagementPage extends StatelessWidget { + const SpaceManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} From db513f916fa983e9b4f714e9c62f29c74484ed3c Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 18 Jun 2025 16:27:50 +0300 Subject: [PATCH 003/105] Refactor schedule components and update imports for garage door and water heater modules --- .../helper/garage_door_helper.dart | 4 +- .../opening_clsoing_time_dialog_body.dart | 5 +- .../schedule__garage_table.dart | 48 +- .../schedule_garage_header.dart | 0 .../schedule_garage_managment_ui.dart | 2 +- .../schedule_garage_mode_buttons.dart | 0 .../schedule_garage_mode_selector.dart | 0 .../schedule_garage_view.dart | 6 +- .../seconds_picker.dart | 0 .../time_out_alarm_dialog_body.dart | 0 .../view/garage_door_control_view.dart | 2 +- .../schedule_device/bloc/schedule_bloc.dart | 557 ++++++++++++++++++ .../schedule_device/bloc/schedule_event.dart | 221 +++++++ .../schedule_device/bloc/schedule_state.dart | 109 ++++ .../schedule_widgets}/count_down_button.dart | 26 +- .../count_down_inching_view.dart | 185 ++++++ .../inching_mode_buttons.dart | 19 +- .../schedule_widgets/schedual_view.dart | 107 ++++ .../schedule_widgets}/schedule_header.dart | 0 .../schedule_managment_ui.dart | 13 +- .../schedule_mode_buttons.dart | 0 .../schedule_mode_selector.dart | 87 +++ .../schedule_widgets}/schedule_table.dart | 138 +++-- .../helper/add_schedule_dialog_helper.dart | 336 +++++------ .../water_heater/models/schedule_entry.dart | 23 +- .../models/water_heater_status_model.dart | 2 +- .../view/water_heater_device_control.dart | 9 +- .../widgets/count_down_inching_view.dart | 223 ------- .../water_heater/widgets/schedual_view.dart | 117 ---- .../widgets/schedule_mode_selector.dart | 86 --- 30 files changed, 1603 insertions(+), 722 deletions(-) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/opening_clsoing_time_dialog_body.dart (88%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule__garage_table.dart (79%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_header.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_managment_ui.dart (93%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_mode_buttons.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_mode_selector.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_view.dart (91%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/seconds_picker.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/time_out_alarm_dialog_body.dart (100%) create mode 100644 lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart create mode 100644 lib/pages/device_managment/schedule_device/bloc/schedule_event.dart create mode 100644 lib/pages/device_managment/schedule_device/bloc/schedule_state.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/count_down_button.dart (72%) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/inching_mode_buttons.dart (82%) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_header.dart (100%) rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_managment_ui.dart (76%) rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_mode_buttons.dart (100%) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_table.dart (58%) delete mode 100644 lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart delete mode 100644 lib/pages/device_managment/water_heater/widgets/schedual_view.dart delete mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart diff --git a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart index 7b133d45..8c9820f9 100644 --- a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart +++ b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart @@ -4,8 +4,8 @@ import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; diff --git a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart b/lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart similarity index 88% rename from lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart rename to lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart index 843bac9b..0cc9485a 100644 --- a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart @@ -1,14 +1,13 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/seconds_picker.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/seconds_picker.dart'; class OpeningAndClosingTimeDialogBody extends StatefulWidget { final ValueChanged onDurationChanged; final GarageDoorBloc bloc; - OpeningAndClosingTimeDialogBody({ + const OpeningAndClosingTimeDialogBody({ required this.onDurationChanged, required this.bloc, }); diff --git a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart similarity index 79% rename from lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart index 07cd9c7a..0bd347ab 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart @@ -26,7 +26,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { Table( border: TableBorder.all( color: ColorsManager.graysColor, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), ), children: [ TableRow( @@ -50,17 +51,21 @@ class ScheduleGarageTableWidget extends StatelessWidget { BlocBuilder( builder: (context, state) { if (state is ScheduleGarageLoadingState) { - return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator())); } - if (state is GarageDoorLoadedState && state.status.schedules?.isEmpty == true) { + if (state is GarageDoorLoadedState && + state.status.schedules?.isEmpty == true) { return _buildEmptyState(context); } else if (state is GarageDoorLoadedState) { return Container( height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: - const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20)), ), child: _buildTableBody(state, context)); } @@ -78,7 +83,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), child: Center( child: Column( @@ -112,7 +118,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { children: [ if (state.status.schedules != null) for (int i = 0; i < state.status.schedules!.length; i++) - _buildScheduleRow(state.status.schedules![i], i, context, state), + _buildScheduleRow( + state.status.schedules![i], i, context, state), ], ), ), @@ -134,7 +141,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { ); } - TableRow _buildScheduleRow(ScheduleModel schedule, int index, BuildContext context, GarageDoorLoadedState state) { + TableRow _buildScheduleRow(ScheduleModel schedule, int index, + BuildContext context, GarageDoorLoadedState state) { return TableRow( children: [ Center( @@ -152,7 +160,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { width: 24, height: 24, child: schedule.enable - ? const Icon(Icons.radio_button_checked, color: ColorsManager.blueColor) + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) : const Icon( Icons.radio_button_unchecked, color: ColorsManager.grayColor, @@ -160,7 +169,9 @@ class ScheduleGarageTableWidget extends StatelessWidget { ), ), ), - Center(child: Text(_getSelectedDays(ScheduleModel.parseSelectedDays(schedule.days)))), + Center( + child: Text(_getSelectedDays( + ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( @@ -170,18 +181,24 @@ class ScheduleGarageTableWidget extends StatelessWidget { TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - GarageDoorDialogHelper.showAddGarageDoorScheduleDialog(context, - schedule: schedule, index: index, isEdit: true); + GarageDoorDialogHelper.showAddGarageDoorScheduleDialog( + context, + schedule: schedule, + index: index, + isEdit: true); }, child: Text( 'Edit', - style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor), + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), ), ), TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - context.read().add(DeleteGarageDoorScheduleEvent( + context + .read() + .add(DeleteGarageDoorScheduleEvent( index: index, scheduleId: schedule.scheduleId, deviceId: state.status.uuid, @@ -189,7 +206,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { }, child: Text( 'Delete', - style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor), + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), ), ), ], diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart similarity index 93% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart index e5819e89..9b2c2b3f 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule__garage_table.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_selector.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_selector.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart similarity index 91% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart index 107c8e0a..7b772135 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_header.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart'; class BuildGarageDoorScheduleView extends StatefulWidget { const BuildGarageDoorScheduleView({super.key, required this.status}); diff --git a/lib/pages/device_managment/garage_door/widgets/seconds_picker.dart b/lib/pages/device_managment/garage_door/schedule_view/seconds_picker.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/seconds_picker.dart rename to lib/pages/device_managment/garage_door/schedule_view/seconds_picker.dart diff --git a/lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart b/lib/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart rename to lib/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart diff --git a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart index ae2fc9e4..30d9bf5d 100644 --- a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart +++ b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_view.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart new file mode 100644 index 00000000..e6a2645d --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -0,0 +1,557 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +part 'schedule_event.dart'; +part 'schedule_state.dart'; + +class ScheduleBloc extends Bloc { + final String deviceId; + + ScheduleBloc({ + required this.deviceId, + }) : super(ScheduleInitial()) { + on(_initializeAddSchedule); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); + on(_getSchedule); + on(_onAddSchedule); + on(_onEditSchedule); + on(_onUpdateSchedule); + on(_onUpdateScheduleMode); + on(_onUpdateCountdownTime); + on(_onUpdateInchingTime); + on(_onStartScheduleEvent); + on(_onStopScheduleEvent); + on(_onDecrementCountdown); + on(_fetchStatus); + on(_onDeleteSchedule); + } + Timer? _countdownTimer; + Duration countdownRemaining = Duration.zero; + + void _onStopScheduleEvent( + StopScheduleEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + _countdownTimer?.cancel(); + + if (event.mode == ScheduleModes.countdown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } else if (event.mode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + countdownRemaining: Duration.zero, + )); + } + } + } + + void _onUpdateScheduleMode( + UpdateScheduleModeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + scheduleMode: event.scheduleMode, + countdownRemaining: Duration.zero, + )); + } + } + + void _onUpdateCountdownTime( + UpdateCountdownTimeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownHours: event.hours, + countdownMinutes: event.minutes, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + void _onUpdateInchingTime( + UpdateInchingTimeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + inchingHours: event.hours, + inchingMinutes: event.minutes, + countdownRemaining: Duration.zero, + )); + } + } + + void _initializeAddSchedule( + ScheduleInitializeAddEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + selectedTime: event.selectedTime, + selectedDays: event.selectedDays ?? List.filled(7, false), + functionOn: event.functionOn ?? false, + isEditing: event.isEditing, + scheduleMode: event.scheduleMode, + countdownRemaining: Duration.zero, + )); + } else { + emit(ScheduleLoaded( + schedules: const [], + selectedTime: event.selectedTime, + selectedDays: event.selectedDays ?? List.filled(7, false), + functionOn: event.functionOn ?? false, + isEditing: event.isEditing, + deviceId: deviceId, + scheduleMode: event.scheduleMode, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, + )); + } + } + + void _updateSelectedTime( + ScheduleUpdateSelectedTimeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + selectedTime: event.selectedTime, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + void _updateSelectedDay( + ScheduleUpdateSelectedDayEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final updatedDays = List.from(currentState.selectedDays); + updatedDays[event.index] = event.value; + emit(currentState.copyWith( + selectedDays: updatedDays, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + void _updateFunctionOn( + ScheduleUpdateFunctionOnEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + functionOn: event.isOn, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + Future _getSchedule( + ScheduleGetEvent event, + Emitter emit, + ) async { + try { + emit(ScheduleLoading()); + final schedules = await DevicesManagementApi().getDeviceSchedules( + deviceId, + event.category, + ); + + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + schedules: schedules, + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + countdownRemaining: Duration.zero, + )); + } else { + emit(ScheduleLoaded( + schedules: schedules, + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + deviceId: deviceId, + scheduleMode: ScheduleModes.schedule, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, + )); + } + } catch (e) { + emit(ScheduleError('Failed to load schedules: $e')); + } + } + + Future _onAddSchedule( + ScheduleAddEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final newSchedule = ScheduleEntry( + category: event.category, + time: event.time, + function: Status(code: 'switch_1', value: event.functionOn), + days: event.selectedDays); + final success = await DevicesManagementApi().addScheduleRecord( + newSchedule, + deviceId, + ); + + if (success) { + add(const ScheduleGetEvent(category: 'switch_1')); + } else { + emit(const ScheduleError('Failed to add schedule')); + } + } + } catch (e) { + emit(ScheduleError('Failed to add schedule: $e')); + } + } + + Future _onEditSchedule( + ScheduleEditEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final updatedSchedule = ScheduleEntry( + scheduleId: event.scheduleId, + category: event.category, + time: event.time, + function: Status(code: 'switch_1', value: event.functionOn), + days: event.selectedDays, + ); + + final success = await DevicesManagementApi().editScheduleRecord( + deviceId, + updatedSchedule, + ); + + if (success) { + add(const ScheduleGetEvent(category: 'switch_1')); + } else { + emit(const ScheduleError('Failed to update schedule')); + } + } + } catch (e) { + emit(ScheduleError('Failed to update schedule: $e')); + } + } + + Future _onUpdateSchedule( + ScheduleUpdateEntryEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final updatedSchedules = currentState.schedules.map((schedule) { + if (schedule.scheduleId == event.scheduleId) { + return schedule.copyWith( + function: Status(code: 'switch_1', value: event.functionOn), + ); + } + return schedule; + }).toList(); + + final success = await DevicesManagementApi().updateScheduleRecord( + enable: event.enable, + uuid: deviceId, + scheduleId: event.scheduleId, + ); + + if (success) { + emit(currentState.copyWith( + schedules: updatedSchedules, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } else { + emit(const ScheduleError('Failed to update schedule status')); + } + } + } catch (e) { + emit(ScheduleError('Failed to update schedule: $e')); + } + } + + Future _onDeleteSchedule( + ScheduleDeleteEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final success = await DevicesManagementApi().deleteScheduleRecord( + deviceId, + event.scheduleId, + ); + + if (success) { + final updatedSchedules = currentState.schedules + .where((s) => s.scheduleId != event.scheduleId) + .toList(); + emit(currentState.copyWith( + schedules: updatedSchedules, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } else { + emit(const ScheduleError('Failed to delete schedule')); + } + } + } catch (e) { + emit(ScheduleError('Failed to delete schedule: $e')); + } + } + + Duration? _currentCountdown; + + Future _onStartScheduleEvent( + StartScheduleEvent event, + Emitter emit, + ) async { + if (state is ScheduleLoaded) { + final totalSeconds = + Duration(hours: event.hours, minutes: event.minutes).inSeconds; + final code = event.mode == ScheduleModes.countdown + ? 'countdown_1' + : 'switch_inching'; + final currentState = state as ScheduleLoaded; + final duration = Duration(seconds: totalSeconds); + _currentCountdown = duration; + emit(currentState.copyWith( + countdownRemaining: duration, + schedules: currentState.schedules.map((schedule) { + if (schedule.function.code == code) { + return schedule.copyWith( + function: Status(code: code, value: totalSeconds), + ); + } + return schedule; + }).toList(), + countdownHours: event.mode == ScheduleModes.countdown ? event.hours : 0, + )); + + final success = await RemoteControlDeviceService().controlDevice( + deviceUuid: deviceId, + status: Status( + code: code, + value: totalSeconds, + ), + ); + + if (success) { + if (code == 'countdown_1') { + final countdownDuration = Duration(seconds: totalSeconds); + + emit( + currentState.copyWith( + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, + ), + ); + + if (countdownDuration.inSeconds > 0) { + _startCountdownTimer(emit, countdownDuration); + } else { + _countdownTimer?.cancel(); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); + } + } else if (code == 'switch_inching') { + final inchingDuration = Duration(seconds: totalSeconds); + emit( + currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + countdownRemaining: inchingDuration, + ), + ); + } + } + } + } + + void _startCountdownTimer( + Emitter emit, + Duration duration, + ) { + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_currentCountdown != null && _currentCountdown! > Duration.zero) { + _currentCountdown = _currentCountdown! - const Duration(seconds: 1); + countdownRemaining = _currentCountdown!; + add(const ScheduleDecrementCountdownEvent()); + } else { + timer.cancel(); + add(StopScheduleEvent( + mode: _currentCountdown == null + ? ScheduleModes.countdown + : ScheduleModes.inching, + deviceId: deviceId, + )); + } + }); + } + + void _onDecrementCountdown( + ScheduleDecrementCountdownEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownRemaining: countdownRemaining, + )); + } + } + + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + Future _fetchStatus( + ScheduleFetchStatusEvent event, + Emitter emit, + ) async { + emit(ScheduleLoading()); + + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + final deviceStatus = + WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + + final scheduleMode = deviceStatus.scheduleMode; + final isCountdown = scheduleMode == ScheduleModes.countdown; + final isInching = scheduleMode == ScheduleModes.inching; + + Duration? countdownRemaining; + var isCountdownActive = false; + var isInchingActive = false; + + if (isCountdown) { + countdownRemaining = Duration( + hours: deviceStatus.countdownHours, + minutes: deviceStatus.countdownMinutes, + ); + isCountdownActive = countdownRemaining > Duration.zero; + } else if (isInching) { + isInchingActive = Duration( + hours: deviceStatus.inchingHours, + minutes: deviceStatus.inchingMinutes, + ) > + Duration.zero; + } + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + scheduleMode: scheduleMode, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: countdownRemaining ?? Duration.zero, + )); + } else { + emit(ScheduleLoaded( + schedules: const [], + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + deviceId: deviceId, + scheduleMode: scheduleMode, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: countdownRemaining ?? Duration.zero, + )); + } + + if (isCountdownActive && countdownRemaining != null) { + _startCountdownTimer(emit, countdownRemaining); + } + } catch (e) { + emit(ScheduleError('Failed to fetch device status: $e')); + } + } + +} diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart new file mode 100644 index 00000000..369ca795 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -0,0 +1,221 @@ +part of 'schedule_bloc.dart'; + +abstract class ScheduleEvent extends Equatable { + const ScheduleEvent(); +} + +class ScheduleInitializeAddEvent extends ScheduleEvent { + final bool isEditing; + final ScheduleModes scheduleMode; + final TimeOfDay? selectedTime; + final List? selectedDays; + final bool? functionOn; + + const ScheduleInitializeAddEvent({ + required this.isEditing, + required this.scheduleMode, + this.selectedTime, + this.selectedDays, + this.functionOn, + }); + + @override + List get props => [ + isEditing, + scheduleMode, + selectedTime, + selectedDays, + functionOn, + ]; +} + +class ScheduleUpdateSelectedTimeEvent extends ScheduleEvent { + final TimeOfDay selectedTime; + + const ScheduleUpdateSelectedTimeEvent(this.selectedTime); + + @override + List get props => [selectedTime]; +} + +class ScheduleUpdateSelectedDayEvent extends ScheduleEvent { + final int index; + final bool value; + + const ScheduleUpdateSelectedDayEvent(this.index, this.value); + + @override + List get props => [index, value]; +} + +class ScheduleUpdateFunctionOnEvent extends ScheduleEvent { + final bool isOn; + + const ScheduleUpdateFunctionOnEvent(this.isOn); + + @override + List get props => [isOn]; +} + +class ScheduleGetEvent extends ScheduleEvent { + final String category; + + const ScheduleGetEvent({required this.category}); + + @override + List get props => [category]; +} + +class ScheduleAddEvent extends ScheduleEvent { + final String category; + final String time; + final List selectedDays; + final bool functionOn; + + const ScheduleAddEvent({ + required this.category, + required this.time, + required this.selectedDays, + required this.functionOn, + }); + + @override + List get props => [category, time, selectedDays, functionOn]; +} + +class ScheduleEditEvent extends ScheduleEvent { + final String scheduleId; + final String category; + final String time; + final List selectedDays; + final bool functionOn; + + const ScheduleEditEvent({ + required this.scheduleId, + required this.category, + required this.time, + required this.selectedDays, + required this.functionOn, + }); + + @override + List get props => [ + scheduleId, + category, + time, + selectedDays, + functionOn, + ]; +} + +class ScheduleDeleteEvent extends ScheduleEvent { + final String scheduleId; + + const ScheduleDeleteEvent(this.scheduleId); + + @override + List get props => [scheduleId]; +} + +class ScheduleUpdateEntryEvent extends ScheduleEvent { + final String scheduleId; + final bool functionOn; + final bool enable; + + const ScheduleUpdateEntryEvent({ + required this.scheduleId, + required this.functionOn, + required this.enable, + }); + + @override + List get props => [scheduleId, functionOn, enable]; +} + +class UpdateScheduleModeEvent extends ScheduleEvent { + final ScheduleModes scheduleMode; + + const UpdateScheduleModeEvent({required this.scheduleMode}); + + @override + List get props => [scheduleMode]; +} + +class UpdateCountdownTimeEvent extends ScheduleEvent { + final int hours; + final int minutes; + + const UpdateCountdownTimeEvent({ + required this.hours, + required this.minutes, + }); + + @override + List get props => [hours, minutes]; +} + +class UpdateInchingTimeEvent extends ScheduleEvent { + final int hours; + final int minutes; + + const UpdateInchingTimeEvent({ + required this.hours, + required this.minutes, + }); + + @override + List get props => [hours, minutes]; +} + +class StartScheduleEvent extends ScheduleEvent { + final ScheduleModes mode; + final int hours; + final int minutes; + + const StartScheduleEvent({ + required this.mode, + required this.hours, + required this.minutes, + }); + + @override + List get props => [mode, hours, minutes]; +} + +class StopScheduleEvent extends ScheduleEvent { + final ScheduleModes mode; + final String deviceId; + + const StopScheduleEvent({ + required this.mode, + required this.deviceId, + }); + + @override + List get props => [mode, deviceId]; +} + +class ScheduleDecrementCountdownEvent extends ScheduleEvent { + const ScheduleDecrementCountdownEvent(); + + @override + List get props => []; +} + +class ScheduleFetchStatusEvent extends ScheduleEvent { + final String deviceId; + + const ScheduleFetchStatusEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class DeleteScheduleEvent extends ScheduleEvent { + final String scheduleId; + + const DeleteScheduleEvent(this.scheduleId); + + @override + List get props => [scheduleId]; +} diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart new file mode 100644 index 00000000..10cd7611 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -0,0 +1,109 @@ +part of 'schedule_bloc.dart'; + +abstract class ScheduleState extends Equatable { + const ScheduleState(); +} + +class ScheduleInitial extends ScheduleState { + @override + List get props => []; +} + +class ScheduleLoading extends ScheduleState { + @override + List get props => []; +} + +class ScheduleLoaded extends ScheduleState { + final List schedules; + final TimeOfDay? selectedTime; + final List selectedDays; + final bool functionOn; + final bool isEditing; + final String deviceId; + final int countdownHours; + final int countdownMinutes; + final bool isCountdownActive; + final int inchingHours; + final int inchingMinutes; + final bool isInchingActive; + final ScheduleModes scheduleMode; + final Duration? countdownRemaining; + + const ScheduleLoaded({ + required this.schedules, + this.selectedTime, + required this.selectedDays, + required this.functionOn, + required this.isEditing, + required this.deviceId, + this.countdownHours = 0, + this.countdownMinutes = 0, + this.isCountdownActive = false, + this.inchingHours = 0, + this.inchingMinutes = 0, + this.isInchingActive = false, + this.scheduleMode = ScheduleModes.countdown, + this.countdownRemaining, + }); + + ScheduleLoaded copyWith({ + List? schedules, + TimeOfDay? selectedTime, + List? selectedDays, + bool? functionOn, + bool? isEditing, + int? countdownHours, + int? countdownMinutes, + bool? isCountdownActive, + int? inchingHours, + int? inchingMinutes, + bool? isInchingActive, + ScheduleModes? scheduleMode, + Duration? countdownRemaining, + }) { + return ScheduleLoaded( + schedules: schedules ?? this.schedules, + selectedTime: selectedTime ?? this.selectedTime, + selectedDays: selectedDays ?? this.selectedDays, + functionOn: functionOn ?? this.functionOn, + isEditing: isEditing ?? this.isEditing, + deviceId: deviceId, + countdownHours: countdownHours ?? this.countdownHours, + countdownMinutes: countdownMinutes ?? this.countdownMinutes, + isCountdownActive: isCountdownActive ?? this.isCountdownActive, + inchingHours: inchingHours ?? this.inchingHours, + inchingMinutes: inchingMinutes ?? this.inchingMinutes, + isInchingActive: isInchingActive ?? this.isInchingActive, + scheduleMode: scheduleMode ?? this.scheduleMode, + countdownRemaining: countdownRemaining ?? this.countdownRemaining, + ); + } + + @override + List get props => [ + schedules, + selectedTime, + selectedDays, + functionOn, + isEditing, + deviceId, + countdownHours, + countdownMinutes, + isCountdownActive, + inchingHours, + inchingMinutes, + isInchingActive, + scheduleMode, + countdownRemaining, + ]; +} + +class ScheduleError extends ScheduleState { + final String error; + + const ScheduleError(this.error); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart similarity index 72% rename from lib/pages/device_managment/water_heater/widgets/count_down_button.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart index e60c7def..4919018c 100644 --- a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CountdownModeButtons extends StatelessWidget { @@ -38,14 +39,10 @@ class CountdownModeButtons extends StatelessWidget { ? DefaultButton( height: 40, onPressed: () { - context - .read() - .add(StopScheduleEvent(deviceId)); - context.read().add( - ToggleWaterHeaterEvent( + context.read().add( + StopScheduleEvent( + mode: ScheduleModes.countdown, deviceId: deviceId, - code: 'countdown_1', - value: 0, ), ); }, @@ -55,12 +52,11 @@ class CountdownModeButtons extends StatelessWidget { : DefaultButton( height: 40, onPressed: () { - context.read().add( - ToggleWaterHeaterEvent( - deviceId: deviceId, - code: 'countdown_1', - value: Duration(hours: hours, minutes: minutes) - .inSeconds, + context.read().add( + StartScheduleEvent( + mode: ScheduleModes.countdown, + hours: hours, + minutes: minutes, ), ); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart new file mode 100644 index 00000000..d45073ec --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownInchingView extends StatefulWidget { + const CountdownInchingView({super.key}); + + @override + State createState() => _CountdownInchingViewState(); +} + +class _CountdownInchingViewState extends State { + late FixedExtentScrollController _hoursController; + late FixedExtentScrollController _minutesController; + + int _lastHours = -1; + int _lastMinutes = -1; + + @override + void initState() { + super.initState(); + _hoursController = FixedExtentScrollController(); + _minutesController = FixedExtentScrollController(); + } + + @override + void dispose() { + _hoursController.dispose(); + _minutesController.dispose(); + super.dispose(); + } + + void _updateControllers(int displayHours, int displayMinutes) { + if (_lastHours != displayHours) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_hoursController.hasClients) { + _hoursController.jumpToItem(displayHours); + } + }); + _lastHours = displayHours; + } + if (_lastMinutes != displayMinutes) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_minutesController.hasClients) { + _minutesController.jumpToItem(displayMinutes); + } + }); + _lastMinutes = displayMinutes; + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is! ScheduleLoaded) return const SizedBox.shrink(); + + final isCountDown = state.scheduleMode == ScheduleModes.countdown; + final isActive = + isCountDown ? state.isCountdownActive : state.isInchingActive; + final displayHours = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inHours + : (isCountDown ? state.countdownHours : state.inchingHours); + final displayMinutes = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inMinutes.remainder(60) + : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + + _updateControllers(displayHours, displayMinutes); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isCountDown ? 'Countdown:' : 'Inching:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 8), + Visibility( + visible: !isCountDown, + child: const Text( + 'Once enabled this feature, each time the device is turned on, ' + 'it will automatically turn off after a preset time.', + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPickerColumn( + context, + 'h', + displayHours, + 100, + _hoursController, + (value) { + if (!isActive) { + context.read().add(UpdateCountdownTimeEvent( + hours: value, minutes: displayMinutes)); + } + }, + isActive: isActive, + ), + const SizedBox(width: 10), + _buildPickerColumn( + context, + 'm', + displayMinutes, + 60, + _minutesController, + (value) { + if (!isActive) { + context.read().add(UpdateCountdownTimeEvent( + hours: displayHours, minutes: value)); + } + }, + isActive: isActive, + ), + ], + ), + ], + ); + }, + ); + } + + Widget _buildPickerColumn( + BuildContext context, + String label, + int initialValue, + int itemCount, + FixedExtentScrollController controller, + ValueChanged onSelected, { + required bool isActive, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListWheelScrollView.useDelegate( + controller: controller, + itemExtent: 40.0, + physics: isActive + ? const NeverScrollableScrollPhysics() + : const FixedExtentScrollPhysics(), + onSelectedItemChanged: isActive ? null : onSelected, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24, + color: isActive ? ColorsManager.grayColor : Colors.black, + ), + ), + ); + }, + childCount: itemCount, + ), + ), + ), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 18, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart similarity index 82% rename from lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart index 8eec5cca..e75c5d46 100644 --- a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart' + hide StopScheduleEvent; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class InchingModeButtons extends StatelessWidget { @@ -38,15 +41,9 @@ class InchingModeButtons extends StatelessWidget { ? DefaultButton( height: 40, onPressed: () { - context - .read() - .add(StopScheduleEvent(deviceId)); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: deviceId, - code: 'switch_inching', - value: 0, - ), + context.read().add( + StopScheduleEvent( + deviceId: deviceId, mode: ScheduleModes.inching), ); }, backgroundColor: Colors.red, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart new file mode 100644 index 00000000..591b114f --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; + +class BuildScheduleView extends StatelessWidget { + const BuildScheduleView({super.key, required this.deviceUuid}); + final String deviceUuid; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ScheduleBloc( + deviceId: deviceUuid, + ) + ..add(const ScheduleGetEvent(category: "switch_1")) + ..add(ScheduleFetchStatusEvent(deviceUuid)), + child: Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 700, + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is ScheduleLoaded) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ScheduleHeader(), + const SizedBox(height: 20), + ScheduleModeSelector( + currentMode: state.scheduleMode, + ), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.schedule) + ScheduleManagementUI( + deviceUuid: deviceUuid, + onAddSchedule: () async { + final entry = await ScheduleDialogHelper + .showAddScheduleDialog( + context, + schedule: null, + isEdit: false, + ); + if (entry != null) { + context.read().add( + ScheduleAddEvent( + category: entry.category, + time: entry.time, + functionOn: entry.function.value, + selectedDays: entry.days, + ), + ); + } + }, + ), + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + const CountdownInchingView(), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.countdown) + CountdownModeButtons( + isActive: state.isCountdownActive, + deviceId: deviceUuid, + hours: state.countdownHours, + minutes: state.countdownMinutes, + ), + if (state.scheduleMode == ScheduleModes.inching) + InchingModeButtons( + isActive: state.isInchingActive, + deviceId: deviceUuid, + hours: state.inchingHours, + minutes: state.inchingMinutes, + ), + if (state.scheduleMode != ScheduleModes.countdown && + state.scheduleMode != ScheduleModes.inching) + ScheduleModeButtons( + onSave: () => Navigator.pop(context), + ), + ], + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_header.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart similarity index 100% rename from lib/pages/device_managment/water_heater/widgets/schedule_header.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart similarity index 76% rename from lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index 1710c439..b60f00b9 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_table.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleManagementUI extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - final Function onAddSchedule; + final String deviceUuid; + final VoidCallback onAddSchedule; const ScheduleManagementUI({ super.key, - required this.state, + required this.deviceUuid, required this.onAddSchedule, }); @@ -28,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget { padding: 2, backgroundColor: ColorsManager.graysColor, borderRadius: 15, - onPressed: () => onAddSchedule(), + onPressed: onAddSchedule, child: Row( children: [ const Icon(Icons.add, color: ColorsManager.primaryColor), @@ -43,7 +42,7 @@ class ScheduleManagementUI extends StatelessWidget { ), ), const SizedBox(height: 20), - ScheduleTableWidget(state: state), + ScheduleTableWidget(deviceUuid: deviceUuid), ], ); } diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart similarity index 100% rename from lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart new file mode 100644 index 00000000..2bcc0957 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleModeSelector extends StatelessWidget { + final ScheduleModes currentMode; + + const ScheduleModeSelector({ + super.key, + required this.currentMode, + }); + + @override + Widget build(BuildContext context) { + final currentMode = context.select( + (bloc) => bloc.state is ScheduleLoaded && + (bloc.state as ScheduleLoaded).scheduleMode != null + ? (bloc.state as ScheduleLoaded).scheduleMode + : ScheduleModes.schedule, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildRadioTile( + context, 'Countdown', ScheduleModes.countdown, currentMode), + _buildRadioTile( + context, 'Schedule', ScheduleModes.schedule, currentMode), + _buildRadioTile( + context, 'Circulate', ScheduleModes.circulate, currentMode), + _buildRadioTile( + context, 'Inching', ScheduleModes.inching, currentMode), + ], + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String label, + ScheduleModes mode, + ScheduleModes currentMode, + ) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: currentMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + context.read().add( + UpdateScheduleModeEvent(scheduleMode: value), + ); + if (value == ScheduleModes.schedule) { + context.read().add( + const ScheduleGetEvent(category: 'switch_1'), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart similarity index 58% rename from lib/pages/device_managment/water_heater/widgets/schedule_table.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 18cbbe5a..4d36e0e2 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -1,23 +1,38 @@ 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/water_heater/bloc/water_heater_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; -import '../helper/add_schedule_dialog_helper.dart'; - class ScheduleTableWidget extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; + final String deviceUuid; + final String category; const ScheduleTableWidget({ super.key, - required this.state, + required this.deviceUuid, + this.category = 'switch_1', }); + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ScheduleBloc( + deviceId: deviceUuid, + )..add(ScheduleGetEvent(category: category)), + child: _ScheduleTableView(), + ); + } +} + +class _ScheduleTableView extends StatelessWidget { + const _ScheduleTableView(); + @override Widget build(BuildContext context) { return Column( @@ -47,17 +62,17 @@ class ScheduleTableWidget extends StatelessWidget { ), ], ), - BlocBuilder( + BlocBuilder( builder: (context, state) { - if (state is ScheduleLoadingState) { + if (state is ScheduleLoading) { return const SizedBox( height: 200, child: Center(child: CircularProgressIndicator())); } - if (state is WaterHeaterDeviceStatusLoaded && - state.schedules.isEmpty) { + if (state is ScheduleLoaded && state.schedules.isEmpty) { return _buildEmptyState(context); - } else if (state is WaterHeaterDeviceStatusLoaded) { + } + if (state is ScheduleLoaded) { return Container( height: 200, decoration: BoxDecoration( @@ -66,11 +81,12 @@ class ScheduleTableWidget extends StatelessWidget { bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), - child: _buildTableBody(state, context)); + child: _buildTableBody(state.schedules, context)); } - return const SizedBox( - height: 200, - ); + if (state is ScheduleError) { + return Center(child: Text(state.error)); + } + return const SizedBox(height: 200); }, ), ], @@ -95,10 +111,10 @@ class ScheduleTableWidget extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Text( 'No schedules added yet', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), ), ), ], @@ -107,8 +123,7 @@ class ScheduleTableWidget extends StatelessWidget { ); } - Widget _buildTableBody( - WaterHeaterDeviceStatusLoaded state, BuildContext context) { + Widget _buildTableBody(List schedules, BuildContext context) { return SizedBox( height: 200, child: SingleChildScrollView( @@ -116,8 +131,8 @@ class ScheduleTableWidget extends StatelessWidget { border: TableBorder.all(color: ColorsManager.graysColor), defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - for (int i = 0; i < state.schedules.length; i++) - _buildScheduleRow(state.schedules[i], i, context, state), + for (int i = 0; i < schedules.length; i++) + _buildScheduleRow(schedules[i], i, context), ], ), ), @@ -139,20 +154,23 @@ class ScheduleTableWidget extends StatelessWidget { ); } - TableRow _buildScheduleRow(ScheduleModel schedule, int index, - BuildContext context, WaterHeaterDeviceStatusLoaded state) { + TableRow _buildScheduleRow( + ScheduleModel schedule, int index, BuildContext context) { return TableRow( children: [ Center( child: GestureDetector( onTap: () { - context.read().add(UpdateScheduleEntryEvent( - index: index, - enable: !schedule.enable, - scheduleId: schedule.scheduleId, - deviceId: state.status.uuid, - functionOn: schedule.function.value, - )); + ///TODO: Implement toggle functionality + + // Toggle enabled state using ScheduleBloc + // context.read().add( + // UpdateScheduleEvent( + // scheduleId: schedule.scheduleId, + // functionOn: schedule.function.value, + // enable: !schedule.enable, + // ), + // ); }, child: SizedBox( width: 24, @@ -179,26 +197,46 @@ class ScheduleTableWidget extends StatelessWidget { TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - ScheduleDialogHelper.showAddScheduleDialog(context, - schedule: schedule, index: index, isEdit: true); + ScheduleDialogHelper.showAddScheduleDialog( + context, + schedule: ScheduleEntry.fromScheduleModel(schedule), + isEdit: true, + ).then((updatedSchedule) { + print('updatedSchedule : $updatedSchedule'); + if (updatedSchedule != null) { + context.read().add( + ScheduleEditEvent( + scheduleId: schedule.scheduleId, + category: schedule.category, + time: updatedSchedule.time, + functionOn: updatedSchedule.function.value, + selectedDays: updatedSchedule.days), + ); + } + }); }, child: Text( 'Edit', - style: context.textTheme.bodySmall! + style: Theme.of(context) + .textTheme + .bodySmall! .copyWith(color: ColorsManager.blueColor), ), ), TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - context.read().add(DeleteScheduleEvent( - index: index, - scheduleId: schedule.scheduleId, - )); + context.read().add( + DeleteScheduleEvent( + schedule.scheduleId, + ), + ); }, child: Text( 'Delete', - style: context.textTheme.bodySmall! + style: Theme.of(context) + .textTheme + .bodySmall! .copyWith(color: ColorsManager.blueColor), ), ), @@ -210,13 +248,15 @@ class ScheduleTableWidget extends StatelessWidget { } String _getSelectedDays(List selectedDays) { - final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr.join(', '); + // Use the same order as in ScheduleDialogHelper + const days = ScheduleDialogHelper.allDays; + return selectedDays + .asMap() + .entries + .where((entry) => entry.value) + .map((entry) => days[entry.key]) + .join(', '); } + + // Removed allDays from here as it is now in ScheduleDialogHelper } diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 9278e396..b09cb48c 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -1,240 +1,210 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; class ScheduleDialogHelper { - static void showAddScheduleDialog(BuildContext context, {ScheduleModel? schedule, int? index, bool? isEdit}) { - final bloc = context.read(); + static const List allDays = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat' + ]; - if (schedule == null) { - bloc.add((const UpdateSelectedTimeEvent(null))); - bloc.add(InitializeAddScheduleEvent( - selectedTime: null, - selectedDays: List.filled(7, false), - functionOn: false, - isEditing: false, - )); - } else { - final time = _convertStringToTimeOfDay(schedule.time); - final selectedDays = _convertDaysStringToBooleans(schedule.days); + static Future showAddScheduleDialog( + BuildContext context, { + ScheduleEntry? schedule, + bool isEdit = false, + }) { + final initialTime = schedule != null + ? _convertStringToTimeOfDay(schedule.time) + : TimeOfDay.now(); + final initialDays = schedule != null + ? _convertDaysStringToBooleans(schedule.days) + : List.filled(7, false); + bool? functionOn = schedule?.function.value ?? true; + TimeOfDay selectedTime = initialTime; + List selectedDays = List.of(initialDays); - bloc.add(InitializeAddScheduleEvent( - selectedTime: time, - selectedDays: selectedDays, - functionOn: schedule.function.value, - isEditing: true, - index: index, - )); - } - - showDialog( + return showDialog( context: context, builder: (ctx) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + return StatefulBuilder( + builder: (ctx, setState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, + const SizedBox(), + Text( + isEdit ? 'Edit Schedule' : 'Add Schedule', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Colors.blue, fontWeight: FontWeight.bold, ), - ), - const SizedBox(), - ], ), - const SizedBox(height: 24), - SizedBox( - width: 150, - height: 40, - child: DefaultButton( - padding: 8, - backgroundColor: ColorsManager.boxColor, - borderRadius: 15, - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: state.selectedTime ?? TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.light( - primary: ColorsManager.primaryColor, - ), - ), - child: child!, - ); - }, - ); - if (time != null) { - bloc.add(UpdateSelectedTimeEvent(time)); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - state.selectedTime == null ? 'Time' : state.selectedTime!.format(context), - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - ), - ), - const Icon( - Icons.access_time, - color: ColorsManager.grayColor, - size: 18, - ), - ], - ), - ), - ), - const SizedBox(height: 16), - _buildDayCheckboxes(context, state.selectedDays, isEdit: isEdit), - const SizedBox(height: 16), - _buildFunctionSwitch(context, state.functionOn, isEdit), + const SizedBox(), ], ), - actions: [ - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[200], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), ), ), - ), - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - if (state.selectedTime != null) { - if (state.isEditing && index != null) { - bloc.add(EditWaterHeaterScheduleEvent( - scheduleId: schedule?.scheduleId ?? '', - category: 'switch_1', - time: state.selectedTime!, - selectedDays: state.selectedDays, - functionOn: state.functionOn, - )); - } else { - bloc.add(AddScheduleEvent( - category: 'switch_1', - time: state.selectedTime!, - selectedDays: state.selectedDays, - functionOn: state.functionOn, - )); - } - Navigator.pop(context); - } - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: ctx, + initialTime: selectedTime, + ); + if (time != null) { + setState(() => selectedTime = time); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedTime.format(context), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.grey), + ), + const Icon(Icons.access_time, + color: Colors.grey, size: 18), + ], ), ), - ], - ); - } - return const SizedBox(); - }, - ), + ), + const SizedBox(height: 16), + _buildDayCheckboxes(ctx, selectedDays, (i, v) { + setState(() => selectedDays[i] = v); + }), + const SizedBox(height: 16), + _buildFunctionSwitch(ctx, functionOn, (v) { + setState(() => functionOn = v); + }), + ], + ), + actions: [ + SizedBox( + width: 100, + child: OutlinedButton( + onPressed: () { + Navigator.pop(ctx, null); + }, + child: const Text('Cancel'), + ), + ), + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + final entry = ScheduleEntry( + category: schedule?.category ?? 'switch_1', + time: _formatTimeOfDayToISO(selectedTime), + function: Status(code: 'switch_1', value: functionOn), + days: _convertSelectedDaysToStrings(selectedDays), + scheduleId: schedule?.scheduleId, + ); + Navigator.pop(ctx, entry); + }, + child: const Text('Save'), + ), + ), + ], + ); + }, ); }, ); } - static TimeOfDay _convertStringToTimeOfDay(String timeString) { - final regex = RegExp(r'^(\d{2}):(\d{2})$'); - final match = regex.firstMatch(timeString); - if (match != null) { - final hour = int.parse(match.group(1)!); - final minute = int.parse(match.group(2)!); - return TimeOfDay(hour: hour, minute: minute); - } else { - throw const FormatException('Invalid time format'); - } + static TimeOfDay _convertStringToTimeOfDay(String iso) { + final dt = DateTime.tryParse(iso); + if (dt != null) return TimeOfDay(hour: dt.hour, minute: dt.minute); + return const TimeOfDay(hour: 9, minute: 0); } static List _convertDaysStringToBooleans(List selectedDays) { final daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List daysBoolean = List.filled(7, false); - - for (int i = 0; i < daysOfWeek.length; i++) { - if (selectedDays.contains(daysOfWeek[i])) { - daysBoolean[i] = true; - } - } - - return daysBoolean; + return daysOfWeek + .map((d) => + selectedDays.map((e) => e.toLowerCase()).contains(d.toLowerCase())) + .toList(); } - static Widget _buildDayCheckboxes(BuildContext context, List selectedDays, {bool? isEdit}) { - final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + static String _formatTimeOfDayToISO(TimeOfDay t) { + final now = DateTime.now(); + final dt = DateTime(now.year, now.month, now.day, t.hour, t.minute); + return dt.toIso8601String(); + } + static List _convertSelectedDaysToStrings(List selectedDays) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List result = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) result.add(allDays[i]); + } + return result; + } + + static Widget _buildDayCheckboxes(BuildContext ctx, List selectedDays, + Function(int, bool) onChanged) { + final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return Row( - children: List.generate(7, (index) { - return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate( + 7, + (index) => Row( children: [ Checkbox( value: selectedDays[index], - onChanged: (bool? value) { - context.read().add(UpdateSelectedDayEvent(index, value!)); - }, + onChanged: (val) => onChanged(index, val!), ), Text(dayLabels[index]), ], - ); - }), + ), + ), ); } - static Widget _buildFunctionSwitch(BuildContext context, bool isOn, bool? isEdit) { + static Widget _buildFunctionSwitch( + BuildContext ctx, bool isOn, Function(bool) onChanged) { return Row( children: [ Text( 'Function:', - style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.grayColor), + style: + Theme.of(ctx).textTheme.bodySmall!.copyWith(color: Colors.grey), ), const SizedBox(width: 10), Radio( value: true, groupValue: isOn, - onChanged: (bool? value) { - context.read().add(const UpdateFunctionOnEvent(true)); - }, + onChanged: (val) => onChanged(true), ), const Text('On'), const SizedBox(width: 10), Radio( value: false, groupValue: isOn, - onChanged: (bool? value) { - context.read().add(const UpdateFunctionOnEvent(false)); - }, + onChanged: (val) => onChanged(false), ), const Text('Off'), ], diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart index a2a109af..d6a530bb 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_entry.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; class ScheduleEntry { final String category; @@ -58,7 +59,8 @@ class ScheduleEntry { String toJson() => json.encode(toMap()); - factory ScheduleEntry.fromJson(String source) => ScheduleEntry.fromMap(json.decode(source)); + factory ScheduleEntry.fromJson(String source) => + ScheduleEntry.fromMap(json.decode(source)); @override bool operator ==(Object other) { @@ -73,6 +75,23 @@ class ScheduleEntry { @override int get hashCode { - return category.hashCode ^ time.hashCode ^ function.hashCode ^ days.hashCode; + return category.hashCode ^ + time.hashCode ^ + function.hashCode ^ + days.hashCode; + } + + // Existing properties and methods + + // Add the fromScheduleModel method + + static ScheduleEntry fromScheduleModel(ScheduleModel scheduleModel) { + return ScheduleEntry( + days: scheduleModel.days, + time: scheduleModel.time, + function: scheduleModel.function, + category: scheduleModel.category, + scheduleId: scheduleModel.scheduleId, + ); } } diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart index c535bda2..bf9ab2cd 100644 --- a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -16,7 +16,7 @@ class WaterHeaterStatusModel extends Equatable { final String cycleTiming; final List schedules; - const WaterHeaterStatusModel({ + const WaterHeaterStatusModel({ required this.uuid, required this.heaterSwitch, required this.countdownHours, diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index f1e56136..1d80fd9f 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -7,7 +7,7 @@ import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.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'; @@ -35,7 +35,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { - return const SizedBox(height: 200, child: Center(child: SizedBox())); + return const SizedBox( + height: 200, child: Center(child: SizedBox())); } }, )); @@ -79,7 +80,9 @@ class WaterHeaterDeviceControlView extends StatelessWidget context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), - child: BuildScheduleView(status: status), + child: BuildScheduleView( + deviceUuid: device.uuid ?? '', + ), )); }, child: DeviceControlsContainer( diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart deleted file mode 100644 index 9c28d4d6..00000000 --- a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; - -class CountdownInchingView extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const CountdownInchingView({ - super.key, - required this.state, - }); - - @override - Widget build(BuildContext context) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isCountDown ? 'Countdown:' : 'Inching:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 8), - Visibility( - visible: !isCountDown, - child: const Text( - 'Once enabled this feature, each time the device is turned on, it will automatically turn off after a preset time.'), - ), - const SizedBox(height: 8), - _hourMinutesWheel(context, state), - ], - ); - } - - Row _hourMinutesWheel( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - late bool isActive; - if (isCountDown && - state.countdownRemaining != null && - state.isCountdownActive == true) { - isActive = true; - } else if (!isCountDown && - state.countdownRemaining != null && - state.isInchingActive == true) { - isActive = true; - } else { - isActive = false; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn( - context, - 'h', - isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'm', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Row _hourMinutesSecondWheel( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - late bool isActive; - if (isCountDown && - state.countdownRemaining != null && - state.isCountdownActive == true) { - isActive = true; - } else if (!isCountDown && - state.countdownRemaining != null && - state.isInchingActive == true) { - isActive = true; - } else { - isActive = false; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn( - context, - 'h', - isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'm', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'S', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Widget _buildPickerColumn( - BuildContext context, - String label, - int initialValue, - int itemCount, - ValueChanged onSelected, { - required bool isActive, - }) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 40, - width: 80, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(8), - ), - child: ListWheelScrollView.useDelegate( - key: ValueKey('$label-$initialValue'), - controller: FixedExtentScrollController( - initialItem: initialValue, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: onSelected, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) { - return Center( - child: Text( - index.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 24, - color: isActive ? ColorsManager.grayColor : Colors.black, - ), - ), - ); - }, - childCount: itemCount, - ), - ), - ), - const SizedBox(width: 8), - Text( - label, - style: const TextStyle( - color: ColorsManager.grayColor, - fontSize: 18, - ), - ), - ], - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart deleted file mode 100644 index 9d4a2497..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart'; - -class BuildScheduleView extends StatefulWidget { - const BuildScheduleView({super.key, required this.status}); - - final WaterHeaterStatusModel status; - - @override - State createState() => _BuildScheduleViewState(); -} - -class _BuildScheduleViewState extends State { - @override - Widget build(BuildContext context) { - final bloc = BlocProvider.of(context); - - return BlocProvider.value( - value: bloc, - child: Dialog( - backgroundColor: Colors.white, - insetPadding: const EdgeInsets.all(20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SizedBox( - width: 700, - child: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ScheduleHeader(), - const SizedBox(height: 20), - ScheduleModeSelector(state: state), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.schedule) - ScheduleManagementUI( - state: state, - onAddSchedule: () { - ScheduleDialogHelper.showAddScheduleDialog( - context, - schedule: null, - index: null, - isEdit: false); - }, - ), - if (state.scheduleMode == ScheduleModes.countdown || - state.scheduleMode == ScheduleModes.inching) - CountdownInchingView(state: state), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.countdown) - CountdownModeButtons( - isActive: state.isCountdownActive ?? false, - deviceId: widget.status.uuid, - hours: state.countdownHours ?? 0, - minutes: state.countdownMinutes ?? 0, - ), - if (state.scheduleMode == ScheduleModes.inching) - InchingModeButtons( - isActive: state.isInchingActive ?? false, - deviceId: widget.status.uuid, - hours: state.inchingHours ?? 0, - minutes: state.inchingMinutes ?? 0, - ), - if (state.scheduleMode != ScheduleModes.countdown && - state.scheduleMode != ScheduleModes.inching) - ScheduleModeButtons( - onSave: () { - Navigator.pop(context); - }, - ), - ], - ); - } - if (state is WaterHeaterLoadingState) { - return const SizedBox( - height: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ScheduleHeader(), - SizedBox( - height: 20, - ), - Center(child: CircularProgressIndicator()), - ], - )); - } - return const SizedBox( - height: 200, - child: ScheduleHeader(), - ); - }, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart deleted file mode 100644 index bb9ddc8f..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; - -class ScheduleModeSelector extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const ScheduleModeSelector({super.key, required this.state}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Type:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildRadioTile( - context, 'Countdown', ScheduleModes.countdown, state), - _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, state), - _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), - ], - ), - ], - ); - } - - Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, - WaterHeaterDeviceStatusLoaded state) { - return Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: mode, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - if (value == ScheduleModes.countdown) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.countdownHours ?? 0, - minutes: state.countdownMinutes ?? 0, - )); - } else if (value == ScheduleModes.inching) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.inchingHours ?? 0, - minutes: state.inchingMinutes ?? 0, - )); - } - - if (value == ScheduleModes.schedule) { - context.read().add( - GetSchedulesEvent( - category: 'switch_1', - uuid: state.status.uuid, - ), - ); - } - } - }, - ), - ), - ); - } -} From 3d95f2bef0ab4817e92f2fb40cd346ef2d878b6e Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 18 Jun 2025 16:40:13 +0300 Subject: [PATCH 004/105] Fix null safety issue by adding null check for functionOn in schedule dialog helper --- .../water_heater/helper/add_schedule_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index b09cb48c..ae7feac9 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -96,7 +96,7 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(ctx, functionOn, (v) { + _buildFunctionSwitch(ctx, functionOn!, (v) { setState(() => functionOn = v); }), ], From 09c44f8a5f85ab06eeeb6fd9f35bcb8262d6f29f Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Thu, 19 Jun 2025 09:33:45 +0300 Subject: [PATCH 005/105] add comment for problem solve --- lib/pages/common/custom_table.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index ac96bce8..d2923700 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -111,6 +111,7 @@ class _DynamicTableState extends State { thumbVisibility: true, trackVisibility: true, child: Scrollbar( + //fixed the horizontal scrollbar issue controller: _horizontalScrollController, thumbVisibility: true, trackVisibility: true, From d895ed74d2a5b02ab5c6e19e79131a0c16f7d89c Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 10:49:06 +0300 Subject: [PATCH 006/105] Add scheduling functionality to device control views with dialog integration --- .../one_gang_glass_switch_control_view.dart | 58 ++++++++++-- .../view/wall_light_device_control.dart | 52 ++++++++++- .../three_gang_glass_switch_control_view.dart | 88 ++++++++++++++++--- .../view/living_room_device_control.dart | 50 +++++++++++ .../two_gang_glass_switch_control_view.dart | 72 ++++++++++++--- .../view/wall_light_device_control.dart | 56 ++++++++++++ 6 files changed, 342 insertions(+), 34 deletions(-) diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 997be513..694c2f6e 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { @@ -76,15 +81,50 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv onChange: (value) {}, showToggle: false, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Scheduling', - icon: Assets.scheduling, - onChange: (value) {}, - showToggle: false, - ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) ], ); } diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index f1861c55..e590adea 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -1,11 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class WallLightDeviceControl extends StatelessWidget @@ -55,7 +61,6 @@ class WallLightDeviceControl extends StatelessWidget mainAxisSpacing: 12, ), children: [ - const SizedBox(), ToggleWidget( value: status.switch1, code: 'switch_1', @@ -69,7 +74,50 @@ class WallLightDeviceControl extends StatelessWidget )); }, ), - const SizedBox(), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) ], ); } diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 21a81df0..7d80f289 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import '../models/three_gang_glass_switch.dart'; -class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { +class ThreeGangGlassSwitchControlView extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const ThreeGangGlassSwitchControlView({required this.deviceId, super.key}); @@ -17,7 +23,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons Widget build(BuildContext context) { return BlocProvider( create: (context) => - ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), + ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId) + ..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is ThreeGangGlassSwitchLoading) { @@ -34,7 +41,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons ); } - Widget _buildStatusControls(BuildContext context, ThreeGangGlassStatusModel status) { + Widget _buildStatusControls( + BuildContext context, ThreeGangGlassStatusModel status) { final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); @@ -107,15 +115,71 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons onChange: (value) {}, showToggle: false, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Scheduling', - icon: Assets.scheduling, - onChange: (value) {}, - showToggle: false, - ), + + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) + // ToggleWidget( + // value: true, + // code: '', + // deviceId: deviceId, + // label: 'Scheduling', + // icon: Assets.scheduling, + // onChange: (value) { + // print('Scheduling clicked'); + // showDialog( + // context: context, + // builder: (ctx) => BlocProvider.value( + // value: BlocProvider.of(context), + // child: BuildScheduleView( + // deviceUuid: deviceId, + // ), + // ), + // ); + // }, + // showToggle: false, + // ), ], ); } diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 731b354c..f4739b88 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class LivingRoomDeviceControlsView extends StatelessWidget @@ -90,6 +96,50 @@ class LivingRoomDeviceControlsView extends StatelessWidget ); }, ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), ], ); } diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 575deeac..cd80f528 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangGlassSwitchControlView extends StatelessWidget @@ -16,8 +21,9 @@ class TwoGangGlassSwitchControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) - ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), + create: (context) => + TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) + ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is TwoGangGlassSwitchLoading) { @@ -92,15 +98,59 @@ class TwoGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Scheduling', - icon: Assets.scheduling, - onChange: (value) {}, - showToggle: false, - ), + // ToggleWidget( + // value: false, + // code: '', + // deviceId: deviceId, + // label: 'Scheduling', + // icon: Assets.scheduling, + // onChange: (value) {}, + // showToggle: false, + // ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) ], ); } diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 882aac3e..05a02a69 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -1,11 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangDeviceControlView extends StatelessWidget @@ -44,6 +50,7 @@ class TwoGangDeviceControlView extends StatelessWidget children: [ SizedBox( width: 200, + height: 120, child: ToggleWidget( value: status.switch1, code: 'switch_1', @@ -60,6 +67,7 @@ class TwoGangDeviceControlView extends StatelessWidget ), SizedBox( width: 200, + height: 120, child: ToggleWidget( value: status.switch2, code: 'switch_2', @@ -74,6 +82,54 @@ class TwoGangDeviceControlView extends StatelessWidget }, ), ), + SizedBox( + width: 200, + height: 120, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + ) ], ), ); From ed2a8f6ba28e63951ef5fa469d69614292aac3d0 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 11:02:23 +0300 Subject: [PATCH 007/105] Refactor border radius implementation in ScheduleGarageTableWidget for consistency --- .../schedule_view/schedule__garage_table.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart index 0bd347ab..525e79c9 100644 --- a/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart @@ -26,8 +26,7 @@ class ScheduleGarageTableWidget extends StatelessWidget { Table( border: TableBorder.all( color: ColorsManager.graysColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), topRight: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), children: [ TableRow( @@ -56,16 +55,15 @@ class ScheduleGarageTableWidget extends StatelessWidget { child: Center(child: CircularProgressIndicator())); } if (state is GarageDoorLoadedState && - state.status.schedules?.isEmpty == true) { + state.status.schedules!.isEmpty) { return _buildEmptyState(context); } else if (state is GarageDoorLoadedState) { return Container( height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(20)), ), child: _buildTableBody(state, context)); } @@ -83,8 +81,7 @@ class ScheduleGarageTableWidget extends StatelessWidget { height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Center( child: Column( From 666c64231fedc8902dc818d7e1aab91253be36c0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:22:37 +0300 Subject: [PATCH 008/105] hides bars in `AqiDistributionChart` where all values are zero. --- .../widgets/aqi_distribution_chart.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 2f3d7ff0..d4a25e9e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -32,8 +32,13 @@ class AqiDistributionChart extends StatelessWidget { } List _buildBarGroups() { - return List.generate(chartData.length, (index) { - final data = chartData[index]; + final groups = []; + for (var i = 0; i < chartData.length; i++) { + final data = chartData[i]; + final isAllZero = data.data.every((d) => d.percentage == 0); + if (isAllZero) { + continue; + } final stackItems = []; double currentY = 0; var isFirstElement = true; @@ -56,13 +61,15 @@ class AqiDistributionChart extends StatelessWidget { currentY += percentageData.percentage + _rodStackItemsSpacing; isFirstElement = false; } - - return BarChartGroupData( - x: index, - barRods: stackItems, - groupVertically: true, + groups.add( + BarChartGroupData( + x: i, + barRods: stackItems, + groupVertically: true, + ), ); - }); + } + return groups; } BarTouchData _barTouchData(BuildContext context) { From 78898968e89572e338c920862034d13c5d91b895 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:23:04 +0300 Subject: [PATCH 009/105] include min in `RangeOfAqiChartsHelper.titlesData.leftTitles`. --- .../modules/air_quality/helpers/range_of_aqi_charts_helper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 21cb2a9e..7e8f3b04 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -40,6 +40,7 @@ abstract final class RangeOfAqiChartsHelper { reservedSize: 70, interval: 50, maxIncluded: false, + minIncluded: true, getTitlesWidget: (value, meta) { final text = value >= 300 ? '301+' : value.toInt().toString(); return Padding( From 7172a0e3fb2ae7aa37db58560316cae57ce026c9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:23:39 +0300 Subject: [PATCH 010/105] Matched aqi charts title's to have the same size no matter what the window size is. --- .../widgets/aqi_distribution_chart_title.dart | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index 926d28e1..f7be6ee3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -19,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget { children: [ ChartsLoadingWidget(isLoading: isLoading), const Expanded( - flex: 3, + flex: 4, child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, @@ -28,23 +28,26 @@ class AqiDistributionChartTitle extends StatelessWidget { ), ), ), - FittedBox( - alignment: AlignmentDirectional.centerEnd, - fit: BoxFit.scaleDown, - child: AqiTypeDropdown( - onChanged: (value) { - if (value != null) { - final bloc = context.read(); - try { - final param = _makeLoadAqiDistributionParam(context, value); - bloc.add(LoadAirQualityDistribution(param)); - } catch (_) { - return; - } finally { - bloc.add(UpdateAqiTypeEvent(value)); + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AqiTypeDropdown( + onChanged: (value) { + if (value != null) { + final bloc = context.read(); + try { + final param = _makeLoadAqiDistributionParam(context, value); + bloc.add(LoadAirQualityDistribution(param)); + } catch (_) { + return; + } finally { + bloc.add(UpdateAqiTypeEvent(value)); + } } - } - }, + }, + ), ), ), ], From ad5ada9d5528fbfc6e85f5865919d5e036ffb5ab Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:24:49 +0300 Subject: [PATCH 011/105] allowed `RangeOfAqiValue` values to be nullable, and if they were null they fallback to zero. --- lib/pages/analytics/models/range_of_aqi.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 0308d564..dfb48ecb 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable { factory RangeOfAqiValue.fromJson(Map json) { return RangeOfAqiValue( type: json['type'] as String, - min: (json['min'] as num).toDouble(), - average: (json['average'] as num).toDouble(), - max: (json['max'] as num).toDouble(), + min: (json['min'] as num? ?? 0).toDouble(), + average: (json['average'] as num? ?? 0).toDouble(), + max: (json['max'] as num? ?? 0).toDouble(), ); } From 8dea89db0eebcdd6d8949e387f35e853496b6e8a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:12:54 +0300 Subject: [PATCH 012/105] Implemented AQI legend. --- .../air_quality/views/air_quality_view.dart | 12 ++++-- .../air_quality/widgets/aqi_legend.dart | 38 +++++++++++++++++++ .../widgets/chart_informative_cell.dart | 4 +- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index b6d403eb..61179d15 100644 --- a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { @@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 32, children: [ + SizedBox( + height: height * 0.1, + child: const AqiLegend(), + ), SizedBox( height: height * 1.2, child: const AirQualityEndSideWidget(), @@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height * 1.1, + height: height * 1.2, child: const Column( children: [ Expanded( @@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 20, children: [ - Expanded(child: RangeOfAqiChartBox()), - Expanded(child: AqiDistributionChartBox()), + Expanded(flex: 2, child: AqiLegend()), + Expanded(flex: 12, child: RangeOfAqiChartBox()), + Expanded(flex: 12, child: AqiDistributionChartBox()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart new file mode 100644 index 00000000..3a00925d --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiLegend extends StatelessWidget { + const AqiLegend({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsetsDirectional.all(20), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 16, + children: RangeOfAqiChartsHelper.gradientData.map((e) { + return Flexible( + flex: 4, + child: FittedBox( + fit: BoxFit.fill, + child: ChartInformativeCell( + color: e.$1, + title: FittedBox( + fit: BoxFit.fill, + child: Text(e.$2), + ), + height: null, + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart index eec31998..f79ecb44 100644 --- a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget { required this.title, required this.color, this.hasBorder = false, + this.height, }); final Widget title; final Color color; final bool hasBorder; + final double? height; @override Widget build(BuildContext context) { return Container( - height: MediaQuery.sizeOf(context).height * 0.0385, + height: height ?? MediaQuery.sizeOf(context).height * 0.0385, padding: const EdgeInsetsDirectional.symmetric( vertical: 8, horizontal: 12, From e4cc5fce50508c9f474129b4ab837d91c6b49267 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:18:18 +0300 Subject: [PATCH 013/105] Increased the size of `AqiDistributionChart` tooltip. --- .../widgets/aqi_distribution_chart.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index d4a25e9e..4807ebbd 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -80,6 +80,7 @@ class AqiDistributionChart extends StatelessWidget { color: ColorsManager.semiTransparentBlack, ), tooltipRoundedRadius: 16, + maxContentWidth: 500, tooltipPadding: const EdgeInsets.all(8), getTooltipItem: (group, groupIndex, rod, rodIndex) { final data = chartData[group.x]; @@ -88,10 +89,13 @@ class AqiDistributionChart extends StatelessWidget { final textStyle = context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, - fontSize: 8, + fontSize: 11, ); for (final percentageData in data.data) { + if (percentageData.percentage == 0) { + continue; + } final percentage = percentageData.percentage.toStringAsFixed(1); final type = percentageData.type[0].toUpperCase() + percentageData.type.substring(1).replaceAll('_', ' '); @@ -105,7 +109,7 @@ class AqiDistributionChart extends StatelessWidget { DateFormat('dd/MM/yyyy').format(data.date), context.textTheme.bodyMedium!.copyWith( color: ColorsManager.blackColor, - fontSize: 9, + fontSize: 12, fontWeight: FontWeight.w600, ), textAlign: TextAlign.start, @@ -125,7 +129,6 @@ class AqiDistributionChart extends StatelessWidget { final leftTitles = titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 20, maxIncluded: false, minIncluded: true, getTitlesWidget: (value, meta) => Padding( @@ -147,7 +150,7 @@ class AqiDistributionChart extends StatelessWidget { final bottomTitles = AxisTitles( sideTitles: SideTitles( - showTitles: true, + showTitles: chartData.isNotEmpty, getTitlesWidget: (value, _) => FittedBox( alignment: AlignmentDirectional.bottomCenter, fit: BoxFit.scaleDown, @@ -155,11 +158,10 @@ class AqiDistributionChart extends StatelessWidget { chartData[value.toInt()].date.day.toString(), style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.lightGreyColor, - fontSize: 8, + fontSize: 12, ), ), ), - reservedSize: 36, ), ); From 5201a65a978f795108423a29a758826e9884b9f3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:19:58 +0300 Subject: [PATCH 014/105] matched sizes of bottom titles in aqi charts. --- .../modules/air_quality/helpers/range_of_aqi_charts_helper.dart | 1 + .../modules/air_quality/widgets/aqi_distribution_chart.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 7e8f3b04..e4aa5b6f 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -23,6 +23,7 @@ abstract final class RangeOfAqiChartsHelper { return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + reservedSize: 36, getTitlesWidget: (value, meta) => Padding( padding: const EdgeInsetsDirectional.only(top: 20.0), child: Text( diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 4807ebbd..e35a05e7 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -162,6 +162,7 @@ class AqiDistributionChart extends StatelessWidget { ), ), ), + reservedSize: 36, ), ); From 23c3bf11f9496dbb73f8b4910fcfbae94c9f1053 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:38:28 +0300 Subject: [PATCH 015/105] Improved alignment of `AqiLocationInfoCell`. --- .../widgets/aqi_location_info_cell.dart | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart index fa0216a1..00233ad3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -47,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget { ), ), Align( - alignment: AlignmentDirectional.bottomEnd, - child: Padding( - padding: const EdgeInsetsDirectional.all(10), - child: SizedBox( - height: 40, - width: 120, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.bottomEnd, - child: Text( - value, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.vividBlue.withValues(alpha: 0.7), - fontWeight: FontWeight.w700, - fontSize: 24, + alignment: AlignmentDirectional.bottomCenter, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: SvgPicture.asset( + svgPath, + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomStart, + ), + ), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: Text( + value, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.vividBlue.withValues( + alpha: 0.7, + ), + fontWeight: FontWeight.w700, + fontSize: 24, + ), + ), ), ), ), - ), - ), - ), - Align( - alignment: AlignmentDirectional.bottomStart, - child: SizedBox.square( - dimension: MediaQuery.sizeOf(context).width * 0.45, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.bottomStart, - child: SvgPicture.asset(svgPath), - ), + ], ), ), ], From 2267d95795413182bdb882b878d5c1575aa1dfb6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 15:46:40 +0300 Subject: [PATCH 016/105] Add schedule saving functionality and update schedule events --- .../schedule_device/bloc/schedule_bloc.dart | 125 +++++++++++++----- .../schedule_device/bloc/schedule_event.dart | 11 +- .../schedule_widgets/schedule_table.dart | 80 ++++++----- lib/services/devices_mang_api.dart | 31 +++++ lib/utils/constants/api_const.dart | 1 + 5 files changed, 186 insertions(+), 62 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index e6a2645d..c4e731db 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -37,28 +37,39 @@ class ScheduleBloc extends Bloc { Timer? _countdownTimer; Duration countdownRemaining = Duration.zero; - void _onStopScheduleEvent( + Future _onStopScheduleEvent( StopScheduleEvent event, Emitter emit, - ) { + ) async { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; - _countdownTimer?.cancel(); - if (event.mode == ScheduleModes.countdown) { - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - isCountdownActive: false, - countdownRemaining: Duration.zero, - )); - } else if (event.mode == ScheduleModes.inching) { - emit(currentState.copyWith( - inchingHours: 0, - inchingMinutes: 0, - isInchingActive: false, - countdownRemaining: Duration.zero, - )); + final success = await RemoteControlDeviceService().controlDevice( + deviceUuid: deviceId, + status: Status( + code: 'countdown_1', + value: 0, + ), + ); + if (success) { + _countdownTimer?.cancel(); + if (event.mode == ScheduleModes.countdown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } else if (event.mode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + countdownRemaining: Duration.zero, + )); + } + } else { + emit(const ScheduleError('Failed to stop schedule')); } } } @@ -241,16 +252,14 @@ class ScheduleBloc extends Bloc { ) async { try { if (state is ScheduleLoaded) { - final newSchedule = ScheduleEntry( + final dateTime = DateTime.parse(event.time); + final success = await DevicesManagementApi().postSchedule( category: event.category, - time: event.time, - function: Status(code: 'switch_1', value: event.functionOn), + deviceId: deviceId, + time: getTimeStampWithoutSeconds(dateTime).toString(), + code: 'switch_1', + value: event.functionOn, days: event.selectedDays); - final success = await DevicesManagementApi().addScheduleRecord( - newSchedule, - deviceId, - ); - if (success) { add(const ScheduleGetEvent(category: 'switch_1')); } else { @@ -268,14 +277,14 @@ class ScheduleBloc extends Bloc { ) async { try { if (state is ScheduleLoaded) { + final dateTime = DateTime.parse(event.time); final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, - time: event.time, + time: getTimeStampWithoutSeconds(dateTime).toString(), function: Status(code: 'switch_1', value: event.functionOn), days: event.selectedDays, ); - final success = await DevicesManagementApi().editScheduleRecord( deviceId, updatedSchedule, @@ -299,10 +308,12 @@ class ScheduleBloc extends Bloc { try { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; + final updatedSchedules = currentState.schedules.map((schedule) { if (schedule.scheduleId == event.scheduleId) { return schedule.copyWith( function: Status(code: 'switch_1', value: event.functionOn), + enable: event.enable, ); } return schedule; @@ -491,10 +502,16 @@ class ScheduleBloc extends Bloc { try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + print(status.status); final deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); - final scheduleMode = deviceStatus.scheduleMode; + final scheduleMode = + deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0 + ? ScheduleModes.countdown + : deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0 + ? ScheduleModes.inching + : ScheduleModes.schedule; final isCountdown = scheduleMode == ScheduleModes.countdown; final isInching = scheduleMode == ScheduleModes.inching; @@ -516,6 +533,10 @@ class ScheduleBloc extends Bloc { Duration.zero; } if (state is ScheduleLoaded) { + print('Updating existing state with fetched status'); + print('scheduleMode: $scheduleMode'); + print('countdownRemaining: $countdownRemaining'); + print('isCountdownActive: $isCountdownActive'); final currentState = state as ScheduleLoaded; emit(currentState.copyWith( scheduleMode: scheduleMode, @@ -546,12 +567,54 @@ class ScheduleBloc extends Bloc { )); } - if (isCountdownActive && countdownRemaining != null) { - _startCountdownTimer(emit, countdownRemaining); - } + // if (isCountdownActive && countdownRemaining != null) { + // _startCountdownTimer(emit, countdownRemaining); + // } } catch (e) { emit(ScheduleError('Failed to fetch device status: $e')); } } + String extractTime(String isoDateTime) { + // Example input: "2025-06-19T15:45:00.000" + return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00" + } + + int? getTimeStampWithoutSeconds(DateTime? dateTime) { + if (dateTime == null) return null; + DateTime dateTimeWithoutSeconds = DateTime(dateTime.year, dateTime.month, + dateTime.day, dateTime.hour, dateTime.minute); + return dateTimeWithoutSeconds.millisecondsSinceEpoch ~/ 1000; + } + + // Future _updateScheduleEvent( + // StatusUpdatedScheduleEvent event, + // Emitter emit, + // ) async { + // if (state is ScheduleLoaded) { + // final currentState = state as ScheduleLoaded; + + // final updatedSchedules = currentState.schedules.map((schedule) { + // if (schedule.scheduleId == event.scheduleId) { + // return schedule.copyWith( + // function: Status(code: 'switch_1', value: event.functionOn), + // enable: event.enable, + // ); + // } + // return schedule; + // }).toList(); + + // bool success = await DevicesManagementApi().updateScheduleRecord( + // enable: event.enable, + // uuid: currentState.status.uuid, + // scheduleId: event.scheduleId, + // ); + + // if (success) { + // emit(currentState.copyWith(schedules: updatedSchedules)); + // } else { + // emit(currentState); + // } + // } + // } } diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 369ca795..5099679c 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -68,7 +68,7 @@ class ScheduleGetEvent extends ScheduleEvent { class ScheduleAddEvent extends ScheduleEvent { final String category; - final String time; + final String time; final List selectedDays; final bool functionOn; @@ -219,3 +219,12 @@ class DeleteScheduleEvent extends ScheduleEvent { @override List get props => [scheduleId]; } + +class StatusUpdatedScheduleEvent extends ScheduleEvent { + final String id; + + const StatusUpdatedScheduleEvent(this.id); + + @override + List get props => [id]; +} diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 4d36e0e2..97ca03e1 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -160,28 +160,26 @@ class _ScheduleTableView extends StatelessWidget { children: [ Center( child: GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { - ///TODO: Implement toggle functionality - - // Toggle enabled state using ScheduleBloc - // context.read().add( - // UpdateScheduleEvent( - // scheduleId: schedule.scheduleId, - // functionOn: schedule.function.value, - // enable: !schedule.enable, - // ), - // ); - }, - child: SizedBox( - width: 24, - height: 24, - child: schedule.enable - ? const Icon(Icons.radio_button_checked, - color: ColorsManager.blueColor) - : const Icon( - Icons.radio_button_unchecked, - color: ColorsManager.grayColor, + context.read().add( + ScheduleUpdateEntryEvent( + scheduleId: schedule.scheduleId, + functionOn: schedule.function.value, + enable: !schedule.enable, ), + ); + }, + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: schedule.enable + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon(Icons.radio_button_unchecked, + color: ColorsManager.grayColor), + ), ), ), ), @@ -202,7 +200,6 @@ class _ScheduleTableView extends StatelessWidget { schedule: ScheduleEntry.fromScheduleModel(schedule), isEdit: true, ).then((updatedSchedule) { - print('updatedSchedule : $updatedSchedule'); if (updatedSchedule != null) { context.read().add( ScheduleEditEvent( @@ -225,12 +222,38 @@ class _ScheduleTableView extends StatelessWidget { ), TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - context.read().add( - DeleteScheduleEvent( - schedule.scheduleId, - ), + onPressed: () async { + final confirmed = await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Confirm Delete'), + content: const Text( + 'Are you sure you want to delete this schedule?'), + actions: [ + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(false), + child: Text('Cancel'), + ), + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(true), + child: const Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), + ), + ], ); + }, + ); + + if (confirmed == true) { + context.read().add( + ScheduleDeleteEvent(schedule.scheduleId), + ); + } }, child: Text( 'Delete', @@ -239,7 +262,7 @@ class _ScheduleTableView extends StatelessWidget { .bodySmall! .copyWith(color: ColorsManager.blueColor), ), - ), + ) ], ), ), @@ -248,7 +271,6 @@ class _ScheduleTableView extends StatelessWidget { } String _getSelectedDays(List selectedDays) { - // Use the same order as in ScheduleDialogHelper const days = ScheduleDialogHelper.allDays; return selectedDays .asMap() @@ -257,6 +279,4 @@ class _ScheduleTableView extends StatelessWidget { .map((entry) => days[entry.key]) .join(', '); } - - // Removed allDays from here as it is now in ScheduleDialogHelper } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6f60e34f..6fb27daf 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:core'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; @@ -386,4 +387,34 @@ class DevicesManagementApi { ); return response; } + + Future postSchedule({ + required String category, + required String deviceId, + required String time, + required String code, + required bool value, + required List days, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.saveSchedule.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + body: jsonEncode( + { + 'category': category, + 'time': time, + 'function': { + 'code': code, + 'value': value, + }, + 'days': days + }, + ), + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 411e72a5..79fd013e 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -136,4 +136,5 @@ abstract class ApiEndpoints { static const String assignDeviceToRoom = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; + static const String saveSchedule = '/schedule/{deviceUuid}'; } From 0b774a6dfca22f356801d83b8630b236661b6076 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 16:20:46 +0300 Subject: [PATCH 017/105] Add scheduling category parameter to BuildScheduleView and update device control dialogs --- .../one_gang_glass_switch_control_view.dart | 3 +- .../view/wall_light_device_control.dart | 3 +- .../schedule_widgets/schedual_view.dart | 6 +- .../three_gang_glass_switch_control_view.dart | 24 +- .../view/living_room_device_control.dart | 143 +++++++++- .../two_gang_glass_switch_control_view.dart | 46 ++++ .../view/wall_light_device_control.dart | 256 ++++++++++++------ .../view/water_heater_device_control.dart | 3 +- 8 files changed, 368 insertions(+), 116 deletions(-) diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 694c2f6e..3f4e6024 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -83,12 +83,13 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', ), )); }, diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index e590adea..e5f2358e 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -76,12 +76,13 @@ class WallLightDeviceControl extends StatelessWidget ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', ), )); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 591b114f..2ae5b869 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -12,8 +12,10 @@ import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_sched import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { - const BuildScheduleView({super.key, required this.deviceUuid}); + const BuildScheduleView( + {super.key, required this.deviceUuid, required this.category}); final String deviceUuid; + final String category; @override Widget build(BuildContext context) { @@ -21,7 +23,7 @@ class BuildScheduleView extends StatelessWidget { create: (_) => ScheduleBloc( deviceId: deviceUuid, ) - ..add(const ScheduleGetEvent(category: "switch_1")) + ..add(ScheduleGetEvent(category: category)) ..add(ScheduleFetchStatusEvent(deviceUuid)), child: Dialog( backgroundColor: Colors.white, diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 7d80f289..79c2138b 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -115,14 +115,14 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( + category: 'switch_1', deviceUuid: deviceId, ), )); @@ -160,26 +160,6 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), ), ) - // ToggleWidget( - // value: true, - // code: '', - // deviceId: deviceId, - // label: 'Scheduling', - // icon: Assets.scheduling, - // onChange: (value) { - // print('Scheduling clicked'); - // showDialog( - // context: context, - // builder: (ctx) => BlocProvider.value( - // value: BlocProvider.of(context), - // child: BuildScheduleView( - // deviceUuid: deviceId, - // ), - // ), - // ); - // }, - // showToggle: false, - // ), ], ); } diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index f4739b88..57c4d397 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -98,12 +98,13 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', ), )); }, @@ -128,13 +129,141 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_3', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Spotlight', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index cd80f528..c4df483b 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -115,6 +115,52 @@ class TwoGangGlassSwitchControlView extends StatelessWidget value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', ), )); }, diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 05a02a69..d1f75564 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -43,92 +43,184 @@ class TwoGangDeviceControlView extends StatelessWidget Widget _buildStatusControls(BuildContext context, TwoGangStatusModel status) { return Center( - child: Wrap( - alignment: WrapAlignment.center, - spacing: 12, - runSpacing: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - width: 200, - height: 120, - child: ToggleWidget( - value: status.switch1, - code: 'switch_1', - deviceId: deviceId, - label: 'Wall Light', - onChange: (value) { - context.read().add(TwoGangSwitchControl( - deviceId: deviceId, - code: 'switch_1', - value: value, - )); - }, - ), - ), - SizedBox( - width: 200, - height: 120, - child: ToggleWidget( - value: status.switch2, - code: 'switch_2', - deviceId: deviceId, - label: 'Ceiling Light', - onChange: (value) { - context.read().add(TwoGangSwitchControl( - deviceId: deviceId, - code: 'switch_2', - value: value, - )); - }, - ), - ), - SizedBox( - width: 200, - height: 120, - child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (ctx) => BlocProvider.value( - value: BlocProvider.of(context), - child: BuildScheduleView( - deviceUuid: deviceId, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + height: 150, + child: ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: 'Wall Light', + onChange: (value) { + context.read().add(TwoGangSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, )); - }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], + }, ), ), - ), + const SizedBox(width: 10), + SizedBox( + width: 200, + height: 150, + child: ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceId, + label: 'Ceiling Light', + onChange: (value) { + context.read().add(TwoGangSwitchControl( + deviceId: deviceId, + code: 'switch_2', + value: value, + )); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + height: 150, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_1', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + color: ColorsManager.blackColor, + fontSize: 12), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + ), + const SizedBox(width: 10), + SizedBox( + width: 200, + height: 150, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + color: ColorsManager.blackColor, + fontSize: 12), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], ) ], ), diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 1d80fd9f..a847e315 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -76,12 +76,13 @@ class WaterHeaterDeviceControlView extends StatelessWidget ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: device.uuid ?? '', + category: 'switch_1', ), )); }, From 8cf73e3efc1aa6afd2ac0c86a5e5a79036240b19 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 16:38:45 +0300 Subject: [PATCH 018/105] Enhance scheduling UI in glass switch control views with improved layout and dialog integration --- .../three_gang_glass_switch_control_view.dart | 164 ++++++++++++++++-- .../two_gang_glass_switch_control_view.dart | 71 +++++--- 2 files changed, 193 insertions(+), 42 deletions(-) diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 79c2138b..bfc0a73e 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -106,14 +106,123 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ); }, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Preferences', - icon: Assets.preferences, - onChange: (value) {}, - showToggle: false, + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_2', + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), ), GestureDetector( onTap: () { @@ -148,18 +257,41 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'SpotLight', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), ), - ) + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Preferences', + icon: Assets.preferences, + onChange: (value) {}, + showToggle: false, + ), ], ); } diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index c4df483b..3c6d7551 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -98,18 +98,9 @@ class TwoGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - // ToggleWidget( - // value: false, - // code: '', - // deviceId: deviceId, - // label: 'Scheduling', - // icon: Assets.scheduling, - // onChange: (value) {}, - // showToggle: false, - // ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), @@ -140,21 +131,35 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), ), ), - GestureDetector( + GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), @@ -185,13 +190,27 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), From 1f82e8411561cdc7582717fc2b466566ae240fc9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 10:55:41 +0300 Subject: [PATCH 019/105] doesnt fetch devices on date change. --- .../helpers/fetch_air_quality_data_helper.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 223c0357..f23abd7b 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -24,11 +24,13 @@ abstract final class FetchAirQualityDataHelper { }) { final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; - loadAnalyticsDevices( - context, - communityUuid: communityUuid, - spaceUuid: spaceUuid, - ); + if (shouldFetchAnalyticsDevices) { + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); + } loadRangeOfAqi( context, spaceUuid: spaceUuid, From 2f233db3326c0d3846b0b87506d1a4e3fb9a7401 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:04:39 +0300 Subject: [PATCH 020/105] implemented space management side bar. --- .../views/space_management_page.dart | 81 ++++++-- .../widgets/space_management_body.dart | 15 ++ .../services/remote_communities_service.dart | 32 +++- .../domain/models/space_model.dart | 16 ++ .../communities_tree_selection_bloc.dart | 47 +++++ .../communities_tree_selection_event.dart | 30 +++ .../communities_tree_selection_state.dart | 29 +++ .../presentation/widgets/community_tile.dart | 37 ++++ .../widgets/create_community_dialog.dart | 181 ++++++++++++++++++ .../space_management_communities_tree.dart | 160 ++++++++++++++++ ...nagement_sidebar_add_community_button.dart | 34 ++++ ...e_management_sidebar_communities_list.dart | 72 +++++++ .../space_management_sidebar_header.dart | 36 ++++ .../presentation/widgets/space_tile.dart | 54 ++++++ .../all_spaces/widgets/space_tile_widget.dart | 3 +- lib/utils/app_routes.dart | 2 +- 16 files changed, 808 insertions(+), 21 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_body.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 03e17165..4c3c7452 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -1,5 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -8,20 +16,67 @@ class SpaceManagementPage extends StatelessWidget { @override Widget build(BuildContext context) { - return WebScaffold( - appBarTitle: Text( - 'Space Management', - style: ResponsiveTextTheme.of(context).deviceManagementTitle, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CommunitiesBloc( + communitiesService: _FakeCommunitiesService(), + )..add(const LoadCommunities(LoadCommunitiesParam())), + ), + BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + ], + child: WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const SpaceManagementBody(), ), - enableMenuSidebar: false, - centerBody: Text( - 'Community Structure', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - rightBody: const NavigateHomeGridView(), - scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} + +class _FakeCommunitiesService extends CommunitiesService { + @override + Future> getCommunity(LoadCommunitiesParam param) async { + return Future.delayed( + const Duration(seconds: 1), + () => [ + const CommunityModel( + uuid: '1', + name: 'Community 1', + spaces: [ + SpaceModel( + uuid: '3', + spaceName: 'Space 1', + icon: 'assets/icons/space.png', + children: [ + SpaceModel( + uuid: '4', + spaceName: 'Space 2', + icon: 'assets/icons/space.png', + children: [], + status: SpaceStatus.active, + ), + ], + status: SpaceStatus.active, + ), + ], + ), + const CommunityModel( + uuid: '2', + name: 'Community 1', + spaces: [], + ), + ], ); } } diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart new file mode 100644 index 00000000..3a9aa3c8 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; + +class SpaceManagementBody extends StatelessWidget { + const SpaceManagementBody({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + SpaceManagementCommunitiesTree(), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 36682bb4..83a212ca 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,9 +1,11 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteCommunitiesService implements CommunitiesService { const RemoteCommunitiesService(this._httpService); @@ -14,13 +16,27 @@ class RemoteCommunitiesService implements CommunitiesService { @override Future> getCommunity(LoadCommunitiesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is not set'); + try { - return _httpService.get( - path: '/api/communities/', - expectedResponseModel: (json) => (json as List) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), + final allCommunities = []; + await _httpService.get( + path: await _makeUrl(), + expectedResponseModel: (json) { + final response = json as Map; + final jsonData = response['data'] as List? ?? []; + return jsonData + .map( + (jsonItem) => CommunityModel.fromJson( + jsonItem as Map, + ), + ) + .toList(); + }, ); + + return allCommunities; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -31,4 +47,10 @@ class RemoteCommunitiesService implements CommunitiesService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is required'); + return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 0f8aadb2..519e8ee7 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,16 +1,31 @@ import 'package:equatable/equatable.dart'; +enum SpaceStatus { + active, + deleted, + parentDeleted; + + static SpaceStatus getValueFromString(String value) => switch (value) { + 'active' => active, + 'deleted' => deleted, + 'parentDeleted' => parentDeleted, + _ => active, + }; +} + class SpaceModel extends Equatable { final String uuid; final String spaceName; final String icon; final List children; + final SpaceStatus status; const SpaceModel({ required this.uuid, required this.spaceName, required this.icon, required this.children, + required this.status, }); factory SpaceModel.fromJson(Map json) { @@ -22,6 +37,7 @@ class SpaceModel extends Equatable { ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], + status: SpaceStatus.getValueFromString(json['status'] as String), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart new file mode 100644 index 00000000..bfc02f11 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -0,0 +1,47 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +part 'communities_tree_selection_event.dart'; +part 'communities_tree_selection_state.dart'; + +class CommunitiesTreeSelectionBloc + extends Bloc { + CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + on(_onSelectCommunity); + on(_onSelectSpace); + on(_onClearSelection); + } + + void _onSelectCommunity( + SelectCommunityEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: null, + ), + ); + } + + void _onSelectSpace( + SelectSpaceEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: null, + selectedSpace: event.space, + ), + ); + } + + void _onClearSelection( + ClearCommunitiesTreeSelectionEvent event, + Emitter emit, + ) { + emit(const CommunitiesTreeSelectionState()); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart new file mode 100644 index 00000000..95ffe173 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -0,0 +1,30 @@ +part of 'communities_tree_selection_bloc.dart'; + +sealed class CommunitiesTreeSelectionEvent extends Equatable { + const CommunitiesTreeSelectionEvent(); + + @override + List get props => []; +} + +final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { + final CommunityModel? community; + + const SelectCommunityEvent({required this.community}); + @override + List get props => [community]; +} + +final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { + final SpaceModel? space; + + const SelectSpaceEvent({required this.space}); + + @override + List get props => [space]; +} + +final class ClearCommunitiesTreeSelectionEvent + extends CommunitiesTreeSelectionEvent { + const ClearCommunitiesTreeSelectionEvent(); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart new file mode 100644 index 00000000..b14d330b --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -0,0 +1,29 @@ +part of 'communities_tree_selection_bloc.dart'; + +final class CommunitiesTreeSelectionState extends Equatable { + const CommunitiesTreeSelectionState({ + this.selectedCommunity, + this.selectedSpace, + }); + + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + + CommunitiesTreeSelectionState copyWith({ + CommunityModel? selectedCommunity, + SpaceModel? selectedSpace, + List? expandedCommunities, + List? expandedSpaces, + }) { + return CommunitiesTreeSelectionState( + selectedCommunity: selectedCommunity ?? this.selectedCommunity, + selectedSpace: selectedSpace ?? this.selectedSpace, + ); + } + + @override + List get props => [ + selectedCommunity, + selectedSpace, + ]; + } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart new file mode 100644 index 00000000..0baaae52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final void Function(String, bool isExpanded) onExpansionChanged; + final void Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + )); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart new file mode 100644 index 00000000..fd8a0a68 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialog extends StatefulWidget { + final void Function(String name) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + State createState() => _CreateCommunityDialogState(); +} + +class _CreateCommunityDialogState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CommunityDialogBloc([]), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + var isNameValid = true; + var isNameEmpty = false; + + if (state is CommunityNameValidationState) { + isNameValid = state.isNameValid; + isNameEmpty = state.isNameEmpty; + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + TextField( + controller: _nameController, + onChanged: (value) { + context + .read() + .add(ValidateCommunityNameEvent(value)); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: isNameValid && !isNameEmpty + ? ColorsManager.boxColor + : ColorsManager.red, + width: 1, + ), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + if (!isNameValid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exists.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + Expanded( + child: DefaultButton( + onPressed: () { + if (isNameValid && !isNameEmpty) { + widget.onCreateCommunity( + _nameController.text.trim(), + ); + Navigator.of(context).pop(); + } + }, + backgroundColor: isNameValid && !isNameEmpty + ? ColorsManager.secondaryColor + : ColorsManager.lightGrayColor, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart new file mode 100644 index 00000000..3248fa7d --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementCommunitiesTree extends StatelessWidget { + const SpaceManagementCommunitiesTree({super.key}); + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } + + static const _width = 300.0; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + width: _width, + decoration: subSectionContainerDecoration, + child: Column( + children: [ + SpaceManagementSidebarHeader( + onAddCommunity: () => _onAddCommunity(context), + ), + CustomSearchBar(onSearchChanged: (value) {}), + const SizedBox(height: 16), + switch (state.status) { + CommunitiesStatus.initial => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.loading => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.success => + _buildCommunitiesTree(context, state.communities), + CommunitiesStatus.failure => Center( + child: Text(state.errorMessage ?? 'Something went wrong'), + ), + }, + ], + ), + ); + }, + ); + } + + Widget _buildCommunitiesTree( + BuildContext context, + List communities, + ) { + return Expanded( + child: SpaceManagementSidebarCommunitiesList( + communities: communities, + itemBuilder: (context, index) { + return _buildCommunityTile(context, communities[index]); + }, + ), + ); + } + + Widget _buildCommunityTile(BuildContext context, CommunityModel community) { + final spaces = community.spaces + .where((space) => space.status == SpaceStatus.active) + .map((space) => _buildSpaceTile( + space: space, + community: community, + context: context, + )) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } + + Widget _buildSpaceTile({ + required SpaceModel space, + required CommunityModel community, + required BuildContext context, + }) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => _buildSpaceTile( + space: childSpace, + community: community, + context: context, + ), + ) + .toList(), + ), + ); + } + + void _onAddCommunity(BuildContext context) => context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + true + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + + void _clearSelection(BuildContext context) => + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (name) {}, + ), + ); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart new file mode 100644 index 00000000..ba281335 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SpaceManagementSidebarAddCommunityButton extends StatelessWidget { + const SpaceManagementSidebarAddCommunityButton({ + required this.onTap, + super.key, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + style: IconButton.styleFrom( + iconSize: 20, + backgroundColor: ColorsManager.circleImageBackground, + shape: const CircleBorder( + side: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 3, + ), + ), + ), + onPressed: onTap, + icon: SvgPicture.asset(Assets.addIcon), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart new file mode 100644 index 00000000..e7cb1ef6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceManagementSidebarCommunitiesList extends StatefulWidget { + const SpaceManagementSidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + + @override + State createState() => + _SpaceManagementSidebarCommunitiesListState(); +} + +class _SpaceManagementSidebarCommunitiesListState + extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd) { + // Call data from API. + return true; + } + + return false; + } + + @override + void dispose() { + _scrollController + ..removeListener(() {}) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: context.screenWidth * 0.5, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.left, + thumbVisibility: true, + controller: _scrollController, + child: NotificationListener( + onNotification: _onNotification, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsetsDirectional.only(start: 16), + itemCount: widget.communities.length, + controller: _scrollController, + itemBuilder: widget.itemBuilder, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart new file mode 100644 index 00000000..cf40f95c --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.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 SpaceManagementSidebarHeader extends StatelessWidget { + const SpaceManagementSidebarHeader({ + required this.onAddCommunity, + super.key, + }); + + final void Function() onAddCommunity; + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Communities', + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + SpaceManagementSidebarAddCommunityButton( + onTap: onAddCommunity, + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart new file mode 100644 index 00000000..d05199f0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final void Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + State createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0), + child: CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart index d72f22ac..d81a3b04 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart @@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; class SpaceTile extends StatefulWidget { final String title; final bool isSelected; - final bool initiallyExpanded; final ValueChanged onExpansionChanged; final List? children; - final Function() onItemSelected; + final void Function() onItemSelected; const SpaceTile({ super.key, diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 51c088d9984f0860d36177cd1645d74d5ac87aa8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:11:25 +0300 Subject: [PATCH 021/105] made communities paginatable. --- .../views/space_management_page.dart | 39 +++----- .../services/remote_communities_service.dart | 25 +++-- .../models/communities_pagination_model.dart | 69 +++++++++++++ .../domain/params/load_communities_param.dart | 33 ++++++- .../domain/services/communities_service.dart | 4 +- .../presentation/bloc/communities_bloc.dart | 99 ++++++++++++++++++- .../presentation/bloc/communities_event.dart | 16 +++ .../presentation/bloc/communities_state.dart | 38 ++++++- 8 files changed, 272 insertions(+), 51 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 4c3c7452..93e2684f 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; @@ -46,37 +46,20 @@ class SpaceManagementPage extends StatelessWidget { class _FakeCommunitiesService extends CommunitiesService { @override - Future> getCommunity(LoadCommunitiesParam param) async { - return Future.delayed( - const Duration(seconds: 1), - () => [ - const CommunityModel( + Future getCommunity(LoadCommunitiesParam param) { + return Future.value(const CommunitiesPaginationModel( + communities: [ + CommunityModel( uuid: '1', name: 'Community 1', - spaces: [ - SpaceModel( - uuid: '3', - spaceName: 'Space 1', - icon: 'assets/icons/space.png', - children: [ - SpaceModel( - uuid: '4', - spaceName: 'Space 2', - icon: 'assets/icons/space.png', - children: [], - status: SpaceStatus.active, - ), - ], - status: SpaceStatus.active, - ), - ], - ), - const CommunityModel( - uuid: '2', - name: 'Community 1', spaces: [], ), ], - ); + page: 1, + size: 10, + hasNext: false, + totalItems: 2, + totalPages: 1, + )); } } diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 83a212ca..e4202398 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -15,28 +15,25 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future> getCommunity(LoadCommunitiesParam param) async { + Future getCommunity(LoadCommunitiesParam param) async { final projectUuid = await ProjectManager.getProjectUUID(); if (projectUuid == null) throw APIException('Project UUID is not set'); try { - final allCommunities = []; - await _httpService.get( + final response = await _httpService.get( path: await _makeUrl(), + queryParameters: { + 'page': param.page, + 'size': param.size, + 'includeSpaces': param.includeSpaces, + if (param.search.isNotEmpty) 'search': param.search, + }, expectedResponseModel: (json) { - final response = json as Map; - final jsonData = response['data'] as List? ?? []; - return jsonData - .map( - (jsonItem) => CommunityModel.fromJson( - jsonItem as Map, - ), - ) - .toList(); + return CommunitiesPaginationModel.fromJson(json as Map); }, ); - return allCommunities; + return response; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart new file mode 100644 index 00000000..f13ef8ba --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; + +class CommunitiesPaginationModel extends Equatable { + const CommunitiesPaginationModel({ + required this.communities, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List communities; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + const CommunitiesPaginationModel.empty() + : communities = const [], + page = 1, + size = 25, + hasNext = false, + totalItems = 0, + totalPages = 0; + + factory CommunitiesPaginationModel.fromJson(Map json) { + return CommunitiesPaginationModel( + communities: (json['data'] as List? ?? []) + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItems'] as int? ?? 0, + totalPages: json['totalPages'] as int? ?? 0, + ); + } + + CommunitiesPaginationModel copyWith({ + List? communities, + int? page, + int? size, + bool? hasNext, + int? totalItems, + int? totalPages, + }) { + return CommunitiesPaginationModel( + communities: communities ?? this.communities, + page: page ?? this.page, + size: size ?? this.size, + hasNext: hasNext ?? this.hasNext, + totalItems: totalItems ?? this.totalItems, + totalPages: totalPages ?? this.totalPages, + ); + } + + @override + List get props => [ + communities, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart index 9bdc215c..774c4c31 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart @@ -1,3 +1,32 @@ -class LoadCommunitiesParam { - const LoadCommunitiesParam(); +import 'package:equatable/equatable.dart'; + +class LoadCommunitiesParam extends Equatable { + const LoadCommunitiesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.includeSpaces = true, + }); + + final int page; + final int size; + final String search; + final bool includeSpaces; + + LoadCommunitiesParam copyWith({ + int? page, + int? size, + String? search, + bool? includeSpaces, + }) { + return LoadCommunitiesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + includeSpaces: includeSpaces ?? this.includeSpaces, + ); + } + + @override + List get props => [page, size, search, includeSpaces]; } diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index bccad2ad..564dc4da 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,6 @@ -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; abstract class CommunitiesService { - Future> getCommunity(LoadCommunitiesParam param); + Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0d85b22f..47dd43f8 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc { }) : _communitiesService = communitiesService, super(const CommunitiesState()) { on(_onLoadCommunities); + on(_onLoadMoreCommunities); + on(_onSearchCommunities); } final CommunitiesService _communitiesService; @@ -23,24 +25,113 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(const CommunitiesState(status: CommunitiesStatus.loading)); - final communities = await _communitiesService.getCommunity(event.param); + emit(state.copyWith(status: CommunitiesStatus.loading)); + + final paginationResponse = await _communitiesService.getCommunity(event.param); + emit( CommunitiesState( status: CommunitiesStatus.success, - communities: communities, + communities: paginationResponse.communities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.param.search, ), ); } on APIException catch (e) { emit( - CommunitiesState( + state.copyWith( status: CommunitiesStatus.failure, errorMessage: e.message, ), ); } catch (e) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onLoadMoreCommunities( + LoadMoreCommunities event, + Emitter emit, + ) async { + if (!state.hasNext || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + + final param = LoadCommunitiesParam( + page: state.currentPage + 1, + search: state.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + + final updatedCommunities = List.from(state.communities) + ..addAll(paginationResponse.communities); + + emit( + state.copyWith( + communities: updatedCommunities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + isLoadingMore: false, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onSearchCommunities( + SearchCommunities event, + Emitter emit, + ) async { + try { + emit(state.copyWith(status: CommunitiesStatus.loading)); + + final param = LoadCommunitiesParam( + page: 1, + search: event.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + emit( CommunitiesState( + status: CommunitiesStatus.success, + communities: paginationResponse.communities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.searchQuery, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( status: CommunitiesStatus.failure, errorMessage: e.toString(), ), diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ef375c5a..aa6eda17 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -15,3 +15,19 @@ class LoadCommunities extends CommunitiesEvent { @override List get props => [param]; } + +class LoadMoreCommunities extends CommunitiesEvent { + const LoadMoreCommunities(); + + @override + List get props => []; +} + +class SearchCommunities extends CommunitiesEvent { + const SearchCommunities(this.searchQuery); + + final String searchQuery; + + @override + List get props => [searchQuery]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart index 94740f0b..c0e57ffd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart @@ -7,12 +7,48 @@ final class CommunitiesState extends Equatable { this.status = CommunitiesStatus.initial, this.communities = const [], this.errorMessage, + this.isLoadingMore = false, + this.hasNext = false, + this.currentPage = 1, + this.searchQuery = '', }); final CommunitiesStatus status; final List communities; final String? errorMessage; + final bool isLoadingMore; + final bool hasNext; + final int currentPage; + final String searchQuery; + + CommunitiesState copyWith({ + CommunitiesStatus? status, + List? communities, + String? errorMessage, + bool? isLoadingMore, + bool? hasNext, + int? currentPage, + String? searchQuery, + }) { + return CommunitiesState( + status: status ?? this.status, + communities: communities ?? this.communities, + errorMessage: errorMessage ?? this.errorMessage, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasNext: hasNext ?? this.hasNext, + currentPage: currentPage ?? this.currentPage, + searchQuery: searchQuery ?? this.searchQuery, + ); + } @override - List get props => [status, communities, errorMessage]; + List get props => [ + status, + communities, + errorMessage, + isLoadingMore, + hasNext, + currentPage, + searchQuery, + ]; } From 65ed94eb089d5c135ce2ec456471a23d7d069efa Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:01:32 +0300 Subject: [PATCH 022/105] debounce and refactored `CommunitiesBloc`. --- .../debounced_communities_service.dart | 60 ++++++++++++ .../presentation/bloc/communities_bloc.dart | 82 ++++------------ .../presentation/bloc/communities_event.dart | 9 -- .../space_management_communities_tree.dart | 97 ++++++++++++++++--- ...e_management_sidebar_communities_list.dart | 44 ++++++++- 5 files changed, 204 insertions(+), 88 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart new file mode 100644 index 00000000..f8bd56d1 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; + +class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService({ + required CommunitiesService communitiesService, + this.debounceDuration = const Duration(milliseconds: 400), + }) : _communitiesService = communitiesService; + + final CommunitiesService _communitiesService; + final Duration debounceDuration; + + Timer? _debounceTimer; + String _lastSearchQuery = ''; + + @override + Future getCommunity( + LoadCommunitiesParam param, + ) async { + if (param.search.isNotEmpty) { + return _getDebouncedCommunity(param); + } + + return _communitiesService.getCommunity(param); + } + + Future _getDebouncedCommunity( + LoadCommunitiesParam param, + ) async { + final completer = Completer(); + + _debounceTimer?.cancel(); + + _lastSearchQuery = param.search; + + _debounceTimer = Timer(debounceDuration, () async { + try { + if (_lastSearchQuery == param.search) { + final result = await _communitiesService.getCommunity(param); + if (!completer.isCompleted) { + completer.complete(result); + } + } else { + if (!completer.isCompleted) { + completer.complete(const CommunitiesPaginationModel.empty()); + } + } + } catch (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + } + }); + + return completer.future; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 47dd43f8..53eb9d3f 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -15,7 +15,6 @@ class CommunitiesBloc extends Bloc { super(const CommunitiesState()) { on(_onLoadCommunities); on(_onLoadMoreCommunities); - on(_onSearchCommunities); } final CommunitiesService _communitiesService; @@ -39,19 +38,9 @@ class CommunitiesBloc extends Bloc { ), ); } on APIException catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } @@ -83,59 +72,30 @@ class CommunitiesBloc extends Bloc { ), ); } on APIException catch (e) { - emit( - state.copyWith( - isLoadingMore: false, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - state.copyWith( - isLoadingMore: false, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } - Future _onSearchCommunities( - SearchCommunities event, + void _onApiException( + APIException e, Emitter emit, - ) async { - try { - emit(state.copyWith(status: CommunitiesStatus.loading)); + ) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } - final param = LoadCommunitiesParam( - page: 1, - search: event.searchQuery, - ); - - final paginationResponse = await _communitiesService.getCommunity(param); - - emit( - CommunitiesState( - status: CommunitiesStatus.success, - communities: paginationResponse.communities, - hasNext: paginationResponse.hasNext, - currentPage: paginationResponse.page, - searchQuery: event.searchQuery, - ), - ); - } on APIException catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); - } + void _onError(Object e, Emitter emit) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index aa6eda17..ae4d86bf 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -22,12 +22,3 @@ class LoadMoreCommunities extends CommunitiesEvent { @override List get props => []; } - -class SearchCommunities extends CommunitiesEvent { - const SearchCommunities(this.searchQuery); - - final String searchQuery; - - @override - List get props => [searchQuery]; -} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 3248fa7d..51322b52 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -1,20 +1,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; import 'package:syncrow_web/utils/style.dart'; -class SpaceManagementCommunitiesTree extends StatelessWidget { +class SpaceManagementCommunitiesTree extends StatefulWidget { const SpaceManagementCommunitiesTree({super.key}); + @override + State createState() => + _SpaceManagementCommunitiesTreeState(); +} + +class _SpaceManagementCommunitiesTreeState + extends State { + @override + void initState() { + context.read().add( + const LoadCommunities(LoadCommunitiesParam()), + ); + super.initState(); + } + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { final selectedSpace = context.read().state.selectedSpace; @@ -25,6 +41,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { return isSpaceSelected || anySubSpaceIsSelected; } + void _onSearchChanged(String searchQuery) { + context.read().add( + LoadCommunities(LoadCommunitiesParam( + search: searchQuery.trim(), + )), + ); + } + + void _onLoadMore() { + context.read().add(const LoadMoreCommunities()); + } + static const _width = 300.0; @override @@ -39,18 +67,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { SpaceManagementSidebarHeader( onAddCommunity: () => _onAddCommunity(context), ), - CustomSearchBar(onSearchChanged: (value) {}), + CustomSearchBar( + onSearchChanged: _onSearchChanged, + ), const SizedBox(height: 16), switch (state.status) { CommunitiesStatus.initial => const Center(child: CircularProgressIndicator()), - CommunitiesStatus.loading => - const Center(child: CircularProgressIndicator()), - CommunitiesStatus.success => - _buildCommunitiesTree(context, state.communities), - CommunitiesStatus.failure => Center( - child: Text(state.errorMessage ?? 'Something went wrong'), - ), + CommunitiesStatus.loading => state.communities.isEmpty + ? const Center(child: CircularProgressIndicator()) + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => _buildErrorState(context, state), }, ], ), @@ -59,15 +87,58 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { ); } + Widget _buildErrorState(BuildContext context, CommunitiesState state) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + state.errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add( + LoadCommunities(LoadCommunitiesParam( + search: state.searchQuery, + )), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + Widget _buildCommunitiesTree( BuildContext context, - List communities, + CommunitiesState state, ) { + if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { + return Expanded( + child: Center( + child: Text( + state.searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "${state.searchQuery}"', + textAlign: TextAlign.center, + ), + ), + ); + } + return Expanded( child: SpaceManagementSidebarCommunitiesList( - communities: communities, + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, itemBuilder: (context, index) { - return _buildCommunityTile(context, communities[index]); + return _buildCommunityTile(context, state.communities[index]); }, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart index e7cb1ef6..68119dcd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -6,11 +6,17 @@ class SpaceManagementSidebarCommunitiesList extends StatefulWidget { const SpaceManagementSidebarCommunitiesList({ required this.communities, required this.itemBuilder, + this.onLoadMore, + this.isLoadingMore = false, + this.hasNext = false, super.key, }); final List communities; final Widget Function(BuildContext context, int index) itemBuilder; + final VoidCallback? onLoadMore; + final bool isLoadingMore; + final bool hasNext; @override State createState() => @@ -25,12 +31,26 @@ class _SpaceManagementSidebarCommunitiesListState void initState() { super.initState(); _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + // Trigger pagination when user is close to the bottom + if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { + widget.onLoadMore!(); + } + } } bool _onNotification(ScrollEndNotification notification) { final hasReachedEnd = notification.metrics.extentAfter == 0; - if (hasReachedEnd) { - // Call data from API. + if (hasReachedEnd && + widget.hasNext && + !widget.isLoadingMore && + widget.onLoadMore != null) { + widget.onLoadMore!(); return true; } @@ -40,13 +60,16 @@ class _SpaceManagementSidebarCommunitiesListState @override void dispose() { _scrollController - ..removeListener(() {}) + ..removeListener(_onScroll) ..dispose(); super.dispose(); } @override Widget build(BuildContext context) { + // Calculate item count including loading indicator + final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); + return SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( @@ -60,9 +83,20 @@ class _SpaceManagementSidebarCommunitiesListState child: ListView.builder( shrinkWrap: true, padding: const EdgeInsetsDirectional.only(start: 16), - itemCount: widget.communities.length, + itemCount: itemCount, controller: _scrollController, - itemBuilder: widget.itemBuilder, + itemBuilder: (context, index) { + if (index == widget.communities.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return widget.itemBuilder(context, index); + }, ), ), ), From d2713c590285b0bc67b65175cb66720fe8bab9a9 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 22 Jun 2025 12:23:09 +0300 Subject: [PATCH 023/105] Add ScheduleControlButton widget and integrate it into water heater and wall light device controls --- .../one_gang_glass_switch_control_view.dart | 55 ++---- .../view/wall_light_device_control.dart | 46 +---- .../schedule_control_button.dart | 72 ++++++++ .../three_gang_glass_switch_control_view.dart | 161 ++---------------- .../view/living_room_device_control.dart | 154 ++--------------- .../two_gang_glass_switch_control_view.dart | 109 ++---------- .../view/wall_light_batch_control.dart | 47 ++++- .../view/wall_light_device_control.dart | 105 +----------- .../view/water_heater_device_control.dart | 40 +---- 9 files changed, 187 insertions(+), 602 deletions(-) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 3f4e6024..1ad5d43b 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { +class OneGangGlassSwitchControlView extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const OneGangGlassSwitchControlView({required this.deviceId, super.key}); @@ -21,7 +19,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv Widget build(BuildContext context) { return BlocProvider( create: (context) => - OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), + OneGangGlassSwitchBlocFactory.create(deviceId: deviceId) + ..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is OneGangGlassSwitchLoading) { @@ -38,7 +37,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv ); } - Widget _buildStatusControls(BuildContext context, OneGangGlassStatusModel status) { + Widget _buildStatusControls( + BuildContext context, OneGangGlassStatusModel status) { final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); @@ -81,51 +81,22 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv onChange: (value) {}, showToggle: false, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - deviceUuid: deviceId, category: 'switch_1', + deviceUuid: deviceId, ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ) + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index e5f2358e..2f6008d2 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class WallLightDeviceControl extends StatelessWidget @@ -74,51 +71,22 @@ class WallLightDeviceControl extends StatelessWidget )); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - deviceUuid: deviceId, category: 'switch_1', + deviceUuid: deviceId, ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ) + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart new file mode 100644 index 00000000..86fc5ba5 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleControlButton extends StatelessWidget { + final VoidCallback onTap; + final String mainText; + final String subtitle; + final String iconPath; + + const ScheduleControlButton({ + super.key, + required this.onTap, + required this.mainText, + required this.subtitle, + required this.iconPath, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + iconPath, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + mainText, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + subtitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index bfc0a73e..72435b74 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; - import '../models/three_gang_glass_switch.dart'; class ThreeGangGlassSwitchControlView extends StatelessWidget @@ -106,7 +102,7 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -118,54 +114,11 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -177,111 +130,25 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - category: 'switch_1', + category: 'switch_3', deviceUuid: deviceId, ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'SpotLight', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'SpotLight', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ToggleWidget( value: false, diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 57c4d397..66784bd5 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class LivingRoomDeviceControlsView extends StatelessWidget @@ -96,7 +93,7 @@ class LivingRoomDeviceControlsView extends StatelessWidget ); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -108,52 +105,11 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -165,53 +121,11 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -223,51 +137,9 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Spotlight', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Spotlight', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ], ); diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 3c6d7551..34b30dd3 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangGlassSwitchControlView extends StatelessWidget @@ -98,7 +95,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -110,54 +107,11 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -169,53 +123,10 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), - ) + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index e8346cb2..849412f2 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; @@ -8,9 +10,11 @@ import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class TwoGangBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const TwoGangBatchControlView({super.key, required this.deviceIds}); final List deviceIds; @@ -18,15 +22,17 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) - ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), + create: (context) => + TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) + ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is TwoGangSwitchLoading) { return const Center(child: CircularProgressIndicator()); } else if (state is TwoGangSwitchStatusLoaded) { return _buildStatusControls(context, state.status); - } else if (state is TwoGangSwitchError || state is TwoGangSwitchControlError) { + } else if (state is TwoGangSwitchError || + state is TwoGangSwitchControlError) { return const Center(child: Text('Error fetching status')); } else { return const Center(child: CircularProgressIndicator()); @@ -82,6 +88,39 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou )); }, ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceIds.first, + ), + )); + }, + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_2', + deviceUuid: deviceIds.first, + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), // FirmwareUpdateWidget( // deviceId: deviceIds.first, // version: 12, diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index d1f75564..ac3fe579 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangDeviceControlView extends StatelessWidget @@ -96,7 +93,7 @@ class TwoGangDeviceControlView extends StatelessWidget SizedBox( width: 200, height: 150, - child: GestureDetector( + child: ScheduleControlButton( onTap: () { showDialog( context: context, @@ -109,58 +106,16 @@ class TwoGangDeviceControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - color: ColorsManager.blackColor, - fontSize: 12), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ), const SizedBox(width: 10), SizedBox( width: 200, height: 150, - child: GestureDetector( + child: ScheduleControlButton( onTap: () { showDialog( context: context, @@ -173,51 +128,9 @@ class TwoGangDeviceControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - color: ColorsManager.blackColor, - fontSize: 12), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ), ], diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index a847e315..16eff86a 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -2,6 +2,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/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; @@ -74,7 +75,7 @@ class WaterHeaterDeviceControlView extends StatelessWidget )); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -86,39 +87,10 @@ class WaterHeaterDeviceControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ) + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } From 8494f0a8f1a484148bdae6afec928c53cf738720 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:21:46 +0300 Subject: [PATCH 024/105] matched community and space models with API. --- .../views/space_management_page.dart | 30 +++-------- .../debounced_communities_service.dart | 51 +++++++------------ .../services/remote_communities_service.dart | 18 ++++--- .../models/communities_pagination_model.dart | 4 +- .../domain/models/community_model.dart | 12 +++++ .../domain/models/space_model.dart | 27 ++++------ .../space_management_communities_tree.dart | 1 - lib/utils/constants/api_const.dart | 1 + 8 files changed, 62 insertions(+), 82 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 93e2684f..957be65a 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -20,7 +20,9 @@ class SpaceManagementPage extends StatelessWidget { providers: [ BlocProvider( create: (context) => CommunitiesBloc( - communitiesService: _FakeCommunitiesService(), + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), )..add(const LoadCommunities(LoadCommunitiesParam())), ), BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), @@ -43,23 +45,3 @@ class SpaceManagementPage extends StatelessWidget { ); } } - -class _FakeCommunitiesService extends CommunitiesService { - @override - Future getCommunity(LoadCommunitiesParam param) { - return Future.value(const CommunitiesPaginationModel( - communities: [ - CommunityModel( - uuid: '1', - name: 'Community 1', - spaces: [], - ), - ], - page: 1, - size: 10, - hasNext: false, - totalItems: 2, - totalPages: 1, - )); - } -} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index f8bd56d1..ca1923f9 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -4,57 +4,44 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; -class DebouncedCommunitiesService implements CommunitiesService { - DebouncedCommunitiesService({ - required CommunitiesService communitiesService, - this.debounceDuration = const Duration(milliseconds: 400), - }) : _communitiesService = communitiesService; +final class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService( + this._decoratee, { + this.debounceDuration = const Duration(milliseconds: 500), + }); - final CommunitiesService _communitiesService; + final CommunitiesService _decoratee; final Duration debounceDuration; Timer? _debounceTimer; - String _lastSearchQuery = ''; + Completer? _completer; @override Future getCommunity( LoadCommunitiesParam param, ) async { - if (param.search.isNotEmpty) { - return _getDebouncedCommunity(param); - } - - return _communitiesService.getCommunity(param); - } - - Future _getDebouncedCommunity( - LoadCommunitiesParam param, - ) async { - final completer = Completer(); - _debounceTimer?.cancel(); - _lastSearchQuery = param.search; + if (_completer != null && !_completer!.isCompleted) { + _completer!.completeError(Exception('Request cancelled by newer request')); + } + + _completer = Completer(); + final currentCompleter = _completer!; _debounceTimer = Timer(debounceDuration, () async { try { - if (_lastSearchQuery == param.search) { - final result = await _communitiesService.getCommunity(param); - if (!completer.isCompleted) { - completer.complete(result); - } - } else { - if (!completer.isCompleted) { - completer.complete(const CommunitiesPaginationModel.empty()); - } + final result = await _decoratee.getCommunity(param); + if (!currentCompleter.isCompleted) { + currentCompleter.complete(result); } } catch (error) { - if (!completer.isCompleted) { - completer.completeError(error); + if (!currentCompleter.isCompleted) { + currentCompleter.completeError(error); } } }); - return completer.future; + return currentCompleter.future; } } diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index e4202398..925a1cd0 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -15,10 +15,9 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future getCommunity(LoadCommunitiesParam param) async { - final projectUuid = await ProjectManager.getProjectUUID(); - if (projectUuid == null) throw APIException('Project UUID is not set'); - + Future getCommunity( + LoadCommunitiesParam param, + ) async { try { final response = await _httpService.get( path: await _makeUrl(), @@ -26,10 +25,12 @@ class RemoteCommunitiesService implements CommunitiesService { 'page': param.page, 'size': param.size, 'includeSpaces': param.includeSpaces, - if (param.search.isNotEmpty) 'search': param.search, + if (param.search.isNotEmpty && param.search != 'null') + 'search': param.search, }, expectedResponseModel: (json) { - return CommunitiesPaginationModel.fromJson(json as Map); + final data = json as Map; + return CommunitiesPaginationModel.fromJson(data); }, ); @@ -48,6 +49,9 @@ class RemoteCommunitiesService implements CommunitiesService { Future _makeUrl() async { final projectUuid = await ProjectManager.getProjectUUID(); if (projectUuid == null) throw APIException('Project UUID is required'); - return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + return ApiEndpoints.getCommunityListv2.replaceAll( + '{projectId}', + projectUuid, + ); } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart index f13ef8ba..a86783be 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart @@ -34,8 +34,8 @@ class CommunitiesPaginationModel extends Equatable { page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 25, hasNext: json['hasNext'] as bool? ?? false, - totalItems: json['totalItems'] as int? ?? 0, - totalPages: json['totalPages'] as int? ?? 0, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, ); } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index c6efad9e..ea0839f9 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -4,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain class CommunityModel extends Equatable { final String uuid; final String name; + final DateTime createdAt; + final DateTime updatedAt; + final String description; + final String externalId; final List spaces; const CommunityModel({ required this.uuid, required this.name, + required this.createdAt, + required this.updatedAt, + required this.description, + required this.externalId, required this.spaces, }); @@ -16,6 +24,10 @@ class CommunityModel extends Equatable { return CommunityModel( uuid: json['uuid'] as String, name: json['name'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + description: json['description'] as String, + externalId: json['externalId'] as String, spaces: (json['spaces'] as List) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 519e8ee7..d6007815 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,43 +1,38 @@ import 'package:equatable/equatable.dart'; -enum SpaceStatus { - active, - deleted, - parentDeleted; - - static SpaceStatus getValueFromString(String value) => switch (value) { - 'active' => active, - 'deleted' => deleted, - 'parentDeleted' => parentDeleted, - _ => active, - }; -} - class SpaceModel extends Equatable { final String uuid; + final DateTime createdAt; + final DateTime updatedAt; final String spaceName; final String icon; final List children; - final SpaceStatus status; + final SpaceModel? parent; const SpaceModel({ required this.uuid, + required this.createdAt, + required this.updatedAt, required this.spaceName, required this.icon, required this.children, - required this.status, + required this.parent, }); factory SpaceModel.fromJson(Map json) { return SpaceModel( uuid: json['uuid'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), spaceName: json['spaceName'] as String, icon: json['icon'] as String, children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], - status: SpaceStatus.getValueFromString(json['status'] as String), + parent: json['parent'] != null + ? SpaceModel.fromJson(json['parent'] as Map) + : null, ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 51322b52..b9902bd6 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -146,7 +146,6 @@ class _SpaceManagementCommunitiesTreeState Widget _buildCommunityTile(BuildContext context, CommunityModel community) { final spaces = community.spaces - .where((space) => space.status == SpaceStatus.active) .map((space) => _buildSpaceTile( space: space, community: community, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index d58d0f28..048f3000 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,6 +46,7 @@ abstract class ApiEndpoints { // Community Module static const String createCommunity = '/projects/{projectId}/communities'; static const String getCommunityList = '/projects/{projectId}/communities'; + static const String getCommunityListv2 = '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity = From 0a424300aa4d17ef8bfe15e237889cba2fbe25cf Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 22 Jun 2025 12:46:54 +0300 Subject: [PATCH 025/105] Refactor ScheduleBloc and related components to use dynamic category handling for schedule events --- .../schedule_device/bloc/schedule_bloc.dart | 47 +++---------------- .../schedule_device/bloc/schedule_event.dart | 4 +- .../schedule_widgets/schedual_view.dart | 3 +- .../schedule_managment_ui.dart | 4 +- .../schedule_mode_selector.dart | 8 ++-- .../schedule_widgets/schedule_table.dart | 1 + 6 files changed, 20 insertions(+), 47 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index c4e731db..62213205 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -257,11 +257,11 @@ class ScheduleBloc extends Bloc { category: event.category, deviceId: deviceId, time: getTimeStampWithoutSeconds(dateTime).toString(), - code: 'switch_1', + code: event.category, value: event.functionOn, days: event.selectedDays); if (success) { - add(const ScheduleGetEvent(category: 'switch_1')); + add(ScheduleGetEvent(category: event.category)); } else { emit(const ScheduleError('Failed to add schedule')); } @@ -282,7 +282,7 @@ class ScheduleBloc extends Bloc { scheduleId: event.scheduleId, category: event.category, time: getTimeStampWithoutSeconds(dateTime).toString(), - function: Status(code: 'switch_1', value: event.functionOn), + function: Status(code: event.category, value: event.functionOn), days: event.selectedDays, ); final success = await DevicesManagementApi().editScheduleRecord( @@ -291,7 +291,9 @@ class ScheduleBloc extends Bloc { ); if (success) { - add(const ScheduleGetEvent(category: 'switch_1')); + add(ScheduleGetEvent( + category: event.category, + )); } else { emit(const ScheduleError('Failed to update schedule')); } @@ -312,7 +314,7 @@ class ScheduleBloc extends Bloc { final updatedSchedules = currentState.schedules.map((schedule) { if (schedule.scheduleId == event.scheduleId) { return schedule.copyWith( - function: Status(code: 'switch_1', value: event.functionOn), + function: Status(code: event.category, value: event.functionOn), enable: event.enable, ); } @@ -533,10 +535,6 @@ class ScheduleBloc extends Bloc { Duration.zero; } if (state is ScheduleLoaded) { - print('Updating existing state with fetched status'); - print('scheduleMode: $scheduleMode'); - print('countdownRemaining: $countdownRemaining'); - print('isCountdownActive: $isCountdownActive'); final currentState = state as ScheduleLoaded; emit(currentState.copyWith( scheduleMode: scheduleMode, @@ -586,35 +584,4 @@ class ScheduleBloc extends Bloc { dateTime.day, dateTime.hour, dateTime.minute); return dateTimeWithoutSeconds.millisecondsSinceEpoch ~/ 1000; } - - // Future _updateScheduleEvent( - // StatusUpdatedScheduleEvent event, - // Emitter emit, - // ) async { - // if (state is ScheduleLoaded) { - // final currentState = state as ScheduleLoaded; - - // final updatedSchedules = currentState.schedules.map((schedule) { - // if (schedule.scheduleId == event.scheduleId) { - // return schedule.copyWith( - // function: Status(code: 'switch_1', value: event.functionOn), - // enable: event.enable, - // ); - // } - // return schedule; - // }).toList(); - - // bool success = await DevicesManagementApi().updateScheduleRecord( - // enable: event.enable, - // uuid: currentState.status.uuid, - // scheduleId: event.scheduleId, - // ); - - // if (success) { - // emit(currentState.copyWith(schedules: updatedSchedules)); - // } else { - // emit(currentState); - // } - // } - // } } diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 5099679c..7ec144fe 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -121,15 +121,17 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent { final String scheduleId; final bool functionOn; final bool enable; + final String category; const ScheduleUpdateEntryEvent({ required this.scheduleId, required this.functionOn, required this.enable, + required this.category, }); @override - List get props => [scheduleId, functionOn, enable]; + List get props => [scheduleId, functionOn, enable, category]; } class UpdateScheduleModeEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 2ae5b869..2fa34559 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -15,7 +15,7 @@ class BuildScheduleView extends StatelessWidget { const BuildScheduleView( {super.key, required this.deviceUuid, required this.category}); final String deviceUuid; - final String category; + final String category; @override Widget build(BuildContext context) { @@ -51,6 +51,7 @@ class BuildScheduleView extends StatelessWidget { const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) ScheduleManagementUI( + category: category, deviceUuid: deviceUuid, onAddSchedule: () async { final entry = await ScheduleDialogHelper diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index b60f00b9..8f871ce4 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -7,11 +7,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleManagementUI extends StatelessWidget { final String deviceUuid; final VoidCallback onAddSchedule; + final String category; const ScheduleManagementUI({ super.key, required this.deviceUuid, required this.onAddSchedule, + this.category = 'switch_1', }); @override @@ -42,7 +44,7 @@ class ScheduleManagementUI extends StatelessWidget { ), ), const SizedBox(height: 20), - ScheduleTableWidget(deviceUuid: deviceUuid), + ScheduleTableWidget(deviceUuid: deviceUuid, category: category), ], ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart index 2bcc0957..25bf7f2c 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -39,10 +39,10 @@ class ScheduleModeSelector extends StatelessWidget { context, 'Countdown', ScheduleModes.countdown, currentMode), _buildRadioTile( context, 'Schedule', ScheduleModes.schedule, currentMode), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, currentMode), - _buildRadioTile( - context, 'Inching', ScheduleModes.inching, currentMode), + // _buildRadioTile( + // context, 'Circulate', ScheduleModes.circulate, currentMode), + // _buildRadioTile( + // context, 'Inching', ScheduleModes.inching, currentMode), ], ), ], diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 97ca03e1..98ae0515 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -164,6 +164,7 @@ class _ScheduleTableView extends StatelessWidget { onTap: () { context.read().add( ScheduleUpdateEntryEvent( + category: schedule.category, scheduleId: schedule.scheduleId, functionOn: schedule.function.value, enable: !schedule.enable, From b79ab06d9523ff11058be8e12a1c1aed30cf2a9d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:46:17 +0300 Subject: [PATCH 026/105] shows a loading indicator when loading. --- .../debounced_communities_service.dart | 6 +-- .../presentation/bloc/communities_bloc.dart | 12 +++++- .../space_management_communities_tree.dart | 41 +++++++++++++------ ...e_management_sidebar_communities_list.dart | 2 - 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index ca1923f9..e512679b 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -14,7 +14,7 @@ final class DebouncedCommunitiesService implements CommunitiesService { final Duration debounceDuration; Timer? _debounceTimer; - Completer? _completer; + late Completer? _completer; @override Future getCommunity( @@ -22,10 +22,6 @@ final class DebouncedCommunitiesService implements CommunitiesService { ) async { _debounceTimer?.cancel(); - if (_completer != null && !_completer!.isCompleted) { - _completer!.completeError(Exception('Request cancelled by newer request')); - } - _completer = Completer(); final currentCompleter = _completer!; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 53eb9d3f..ef91baa2 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -24,9 +24,13 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(state.copyWith(status: CommunitiesStatus.loading)); + emit( + state.copyWith(status: CommunitiesStatus.loading), + ); - final paginationResponse = await _communitiesService.getCommunity(event.param); + final paginationResponse = await _communitiesService.getCommunity( + event.param, + ); emit( CommunitiesState( @@ -35,6 +39,7 @@ class CommunitiesBloc extends Bloc { hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, searchQuery: event.param.search, + isLoadingMore: false, ), ); } on APIException catch (e) { @@ -65,6 +70,7 @@ class CommunitiesBloc extends Bloc { emit( state.copyWith( + status: CommunitiesStatus.success, communities: updatedCommunities, hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, @@ -84,6 +90,7 @@ class CommunitiesBloc extends Bloc { ) { emit( state.copyWith( + status: CommunitiesStatus.failure, isLoadingMore: false, errorMessage: e.message, ), @@ -93,6 +100,7 @@ class CommunitiesBloc extends Bloc { void _onError(Object e, Emitter emit) { emit( state.copyWith( + status: CommunitiesStatus.failure, isLoadingMore: false, errorMessage: e.toString(), ), diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index b9902bd6..4501cf7e 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -42,11 +42,9 @@ class _SpaceManagementCommunitiesTreeState } void _onSearchChanged(String searchQuery) { - context.read().add( - LoadCommunities(LoadCommunitiesParam( - search: searchQuery.trim(), - )), - ); + context + .read() + .add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim()))); } void _onLoadMore() { @@ -80,6 +78,13 @@ class _SpaceManagementCommunitiesTreeState CommunitiesStatus.success => _buildCommunitiesTree(context, state), CommunitiesStatus.failure => _buildErrorState(context, state), }, + Visibility( + visible: state.isLoadingMore, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Center(child: CircularProgressIndicator()), + ), + ), ], ), ); @@ -132,14 +137,24 @@ class _SpaceManagementCommunitiesTreeState } return Expanded( - child: SpaceManagementSidebarCommunitiesList( - communities: state.communities, - onLoadMore: state.hasNext ? _onLoadMore : null, - isLoadingMore: state.isLoadingMore, - hasNext: state.hasNext, - itemBuilder: (context, index) { - return _buildCommunityTile(context, state.communities[index]); - }, + child: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return _buildCommunityTile(context, state.communities[index]); + }, + ), + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const Center(child: CircularProgressIndicator()), + ), + ], ), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart index 68119dcd..40766be5 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -37,7 +37,6 @@ class _SpaceManagementSidebarCommunitiesListState void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) { - // Trigger pagination when user is close to the bottom if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { widget.onLoadMore!(); } @@ -67,7 +66,6 @@ class _SpaceManagementSidebarCommunitiesListState @override Widget build(BuildContext context) { - // Calculate item count including loading indicator final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); return SingleChildScrollView( From f02788eaa5835255e7707f266e53cea6dd860c11 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 14:58:38 +0300 Subject: [PATCH 027/105] implemented create community feature. --- .../domain/models/community_model.dart | 4 +- .../presentation/bloc/communities_bloc.dart | 8 + .../presentation/bloc/communities_event.dart | 9 + .../widgets/create_community_dialog.dart | 181 ------------------ .../space_management_communities_tree.dart | 33 ++-- .../remote_create_community_service.dart | 50 +++-- .../domain/param/create_community_param.dart | 8 +- .../presentation/create_community_dialog.dart | 61 ++++++ .../create_community_dialog_widget.dart | 144 ++++++++++++++ .../create_community_name_text_field.dart | 48 +++++ 10 files changed, 338 insertions(+), 208 deletions(-) delete mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index ea0839f9..344dbff5 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -27,8 +27,8 @@ class CommunityModel extends Equatable { createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), description: json['description'] as String, - externalId: json['externalId'] as String, - spaces: (json['spaces'] as List) + externalId: json['externalId']?.toString() ?? '', + spaces: (json['spaces'] as List? ?? []) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index ef91baa2..0f754b06 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -15,6 +15,7 @@ class CommunitiesBloc extends Bloc { super(const CommunitiesState()) { on(_onLoadCommunities); on(_onLoadMoreCommunities); + on(_onInsertCommunity); } final CommunitiesService _communitiesService; @@ -106,4 +107,11 @@ class CommunitiesBloc extends Bloc { ), ); } + + void _onInsertCommunity( + InsertCommunity event, + Emitter emit, + ) { + emit(state.copyWith(communities: [event.community, ...state.communities])); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ae4d86bf..cd14fa3d 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -22,3 +22,12 @@ class LoadMoreCommunities extends CommunitiesEvent { @override List get props => []; } + +final class InsertCommunity extends CommunitiesEvent { + const InsertCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart deleted file mode 100644 index fd8a0a68..00000000 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class CreateCommunityDialog extends StatefulWidget { - final void Function(String name) onCreateCommunity; - final String? initialName; - final Widget title; - - const CreateCommunityDialog({ - super.key, - required this.onCreateCommunity, - required this.title, - this.initialName, - }); - - @override - State createState() => _CreateCommunityDialogState(); -} - -class _CreateCommunityDialogState extends State { - late final TextEditingController _nameController; - - @override - void initState() { - _nameController = TextEditingController(text: widget.initialName ?? ''); - super.initState(); - } - - @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CommunityDialogBloc([]), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: ColorsManager.transparentColor, - child: Stack( - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.3, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.25), - blurRadius: 20, - spreadRadius: 5, - offset: const Offset(0, 5), - ), - ], - ), - child: SingleChildScrollView( - child: BlocBuilder( - builder: (context, state) { - var isNameValid = true; - var isNameEmpty = false; - - if (state is CommunityNameValidationState) { - isNameValid = state.isNameValid; - isNameEmpty = state.isNameEmpty; - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle( - style: Theme.of(context).textTheme.headlineMedium!, - child: widget.title, - ), - const SizedBox(height: 18), - TextField( - controller: _nameController, - onChanged: (value) { - context - .read() - .add(ValidateCommunityNameEvent(value)); - }, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - hintText: 'Please enter the community name', - filled: true, - fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: isNameValid && !isNameEmpty - ? ColorsManager.boxColor - : ColorsManager.red, - width: 1, - ), - borderRadius: BorderRadius.circular(10), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - width: 1.5, - ), - ), - ), - ), - if (!isNameValid) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name already exists.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - if (isNameEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name should not be empty.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: CancelButton( - label: 'Cancel', - onPressed: () => Navigator.of(context).pop(), - ), - ), - const SizedBox(width: 16), - Expanded( - child: DefaultButton( - onPressed: () { - if (isNameValid && !isNameEmpty) { - widget.onCreateCommunity( - _nameController.text.trim(), - ); - Navigator.of(context).pop(); - } - }, - backgroundColor: isNameValid && !isNameEmpty - ? ColorsManager.secondaryColor - : ColorsManager.lightGrayColor, - borderRadius: 10, - foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), - ), - ), - ], - ), - ], - ); - }, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 4501cf7e..efafdd85 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -7,10 +7,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -220,15 +220,17 @@ class _SpaceManagementCommunitiesTreeState ); } - void _onAddCommunity(BuildContext context) => context - .read() - .state - .selectedCommunity - ?.uuid - .isNotEmpty ?? - true - ? _clearSelection(context) - : _showCreateCommunityDialog(context); + void _onAddCommunity(BuildContext context) { + context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + false + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + } void _clearSelection(BuildContext context) => context.read().add( @@ -237,9 +239,16 @@ class _SpaceManagementCommunitiesTreeState void _showCreateCommunityDialog(BuildContext context) => showDialog( context: context, - builder: (context) => CreateCommunityDialog( + builder: (_) => CreateCommunityDialog( title: const Text('Community Name'), - onCreateCommunity: (name) {}, + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, ), ); } diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index be83124b..bd91f6ce 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart'; @@ -16,24 +17,51 @@ class RemoteCreateCommunityService implements CreateCommunityService { Future createCommunity(CreateCommunityParam param) async { try { final response = await _httpService.post( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + path: await _makeUrl(), + body: { + 'name': param.name, + 'description': param.description, + }, + expectedResponseModel: (data) { + final json = data as Map; + if (json['success'] == true) { + return CommunityModel.fromJson( + json['data'] as Map, + ); + } + return null; + }, ); + + if (response == null) { + throw APIException( + _getErrorMessageFromBody(response as Map?), + ); + } return response; } on DioException catch (e) { final message = e.response?.data as Map?; - final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; - final formattedErrorMessage = [ - _defaultErrorMessage, - errorMessage, - ].join(': '); - throw APIException(formattedErrorMessage); + throw APIException(_getErrorMessageFromBody(message)); } catch (e) { final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); throw APIException(formattedErrorMessage); } } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) { + return _defaultErrorMessage; + } + final error = body['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities'; + } } diff --git a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart index 3d7c203b..68a9fa11 100644 --- a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart +++ b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; class CreateCommunityParam extends Equatable { - const CreateCommunityParam({required this.name}); - + const CreateCommunityParam({ + required this.name, + this.description = '', + }); + final String name; + final String description; @override List get props => [name]; diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart new file mode 100644 index 00000000..8c1d474d --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class CreateCommunityDialog extends StatelessWidget { + final void Function(CommunityModel community) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), + child: BlocListener( + listener: (context, state) { + switch (state) { + case CreateCommunityLoading(): + showDialog( + context: context, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + break; + case CreateCommunitySuccess(:final community): + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Community created successfully')), + ); + onCreateCommunity.call(community); + break; + case CreateCommunityFailure(:final message): + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + break; + default: + break; + } + }, + child: CreateCommunityDialogWidget( + title: title, + initialName: initialName, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart new file mode 100644 index 00000000..49d43ae6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialogWidget extends StatefulWidget { + final String? initialName; + final Widget title; + + const CreateCommunityDialogWidget({ + super.key, + required this.title, + this.initialName, + }); + + @override + State createState() => + _CreateCommunityDialogWidgetState(); +} + +class _CreateCommunityDialogWidgetState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withValues(alpha: 0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField( + nameController: _nameController, + ), + if (state case CreateCommunityFailure(:final message)) + Padding( + padding: const EdgeInsets.only(top: 18), + child: SelectableText( + '* $message', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Row _buildActionButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + _buildCreateCommunityButton(context), + ], + ); + } + + Widget _buildCreateCommunityButton(BuildContext context) { + return Expanded( + child: DefaultButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _onSubmit(context); + } + }, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } + + void _onSubmit(BuildContext context) { + if (_formKey.currentState?.validate() ?? false) { + context.read().add( + CreateCommunity( + CreateCommunityParam( + name: _nameController.text.trim(), + ), + ), + ); + } + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart new file mode 100644 index 00000000..d42474d5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CreateCommunityNameTextField extends StatelessWidget { + const CreateCommunityNameTextField({ + required this.nameController, + super.key, + }); + + final TextEditingController nameController; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: nameController, + validator: _validator, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: _buildBorder(ColorsManager.boxColor), + focusedBorder: _buildBorder(), + focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error), + errorBorder: _buildBorder(Theme.of(context).colorScheme.error), + ), + ); + } + + String? _validator(String? value) { + if (value == null || value.isEmpty) { + return '*Name should not be empty.'; + } + + return null; + } + + InputBorder _buildBorder([Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5), + width: 1, + ), + ); + } +} From 09446844b0e689b829ad586ac6f09cd1eea46e1c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 15:11:38 +0300 Subject: [PATCH 028/105] reverted initializing the new space management page in the router, to avoid any confusion with the QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From a793cc3967952b5cb4b0d1d46acfcb31a7795654 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Sun, 22 Jun 2025 15:24:53 +0300 Subject: [PATCH 029/105] fix it and add lock to open when press (as loved simple animation) with adding the timer as circle --- .../door_lock/bloc/door_lock_bloc.dart | 97 +++++-------------- .../door_lock/widget/door_button.dart | 89 +++++------------ 2 files changed, 48 insertions(+), 138 deletions(-) diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart index f83ced1a..f6cebe4d 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart @@ -1,5 +1,3 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member - import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,45 +14,38 @@ class DoorLockBloc extends Bloc { DoorLockBloc({required this.deviceId}) : super(DoorLockInitial()) { on(_onFetchDeviceStatus); - //on(_onDoorLockControl); on(_updateLock); on(_onFactoryReset); on(_onStatusUpdated); } - _listenToChanges(deviceId) { + void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final data = event.snapshot.value; + if (data is Map) { + final statusData = data['status'] as List? ?? []; + final statusList = statusData.map((item) { + return Status(code: item['code'], value: item['value']); + }).toList(); - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - DoorLockStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final model = + DoorLockStatusModel.fromJson(data['productUuid'], statusList); + if (!isClosed) { + add(StatusUpdated(model)); + } } }); } catch (_) {} } void _onStatusUpdated(StatusUpdated event, Emitter emit) { - emit(DoorLockStatusLoading()); - deviceStatus = event.deviceStatus; emit(DoorLockStatusLoaded(deviceStatus)); } - FutureOr _onFetchDeviceStatus( + Future _onFetchDeviceStatus( DoorLockFetchStatus event, Emitter emit) async { emit(DoorLockStatusLoading()); try { @@ -63,14 +54,13 @@ class DoorLockBloc extends Bloc { deviceStatus = DoorLockStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); - emit(DoorLockStatusLoaded(deviceStatus)); } catch (e) { emit(DoorLockControlError(e.toString())); } } - FutureOr _updateLock( + Future _updateLock( UpdateLockEvent event, Emitter emit) async { final oldValue = deviceStatus.normalOpenSwitch; deviceStatus = deviceStatus.copyWith(normalOpenSwitch: !oldValue); @@ -78,7 +68,6 @@ class DoorLockBloc extends Bloc { try { final response = await DevicesManagementApi.openDoorLock(deviceId); - if (!response) { _revertValueAndEmit(deviceId, 'normal_open_switch', oldValue, emit); } @@ -88,35 +77,8 @@ class DoorLockBloc extends Bloc { } } - Future _runDebounce({ - required String deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - }) async { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - if (!response) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit( - String deviceId, - String code, - dynamic oldValue, - Emitter emit, - ) { + void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, + Emitter emit) { _updateLocalValue(code, oldValue); emit(DoorLockStatusLoaded(deviceStatus)); emit(const DoorLockControlError('Failed to control the device.')); @@ -124,34 +86,23 @@ class DoorLockBloc extends Bloc { void _updateLocalValue(String code, dynamic value) { switch (code) { - case 'reverse_lock': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(reverseLock: value); - } - break; case 'normal_open_switch': if (value is bool) { deviceStatus = deviceStatus.copyWith(normalOpenSwitch: value); } break; + case 'reverse_lock': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(reverseLock: value); + } + break; default: break; } emit(DoorLockStatusLoaded(deviceStatus)); } - dynamic _getValueByCode(String code) { - switch (code) { - case 'reverse_lock': - return deviceStatus.reverseLock; - case 'normal_open_switch': - return deviceStatus.normalOpenSwitch; - default: - return null; - } - } - - FutureOr _onFactoryReset( + Future _onFactoryReset( DoorLockFactoryReset event, Emitter emit) async { emit(DoorLockStatusLoading()); try { diff --git a/lib/pages/device_managment/door_lock/widget/door_button.dart b/lib/pages/device_managment/door_lock/widget/door_button.dart index e8e3066e..c1ac7bc0 100644 --- a/lib/pages/device_managment/door_lock/widget/door_button.dart +++ b/lib/pages/device_managment/door_lock/widget/door_button.dart @@ -8,7 +8,7 @@ import 'package:syncrow_web/pages/device_managment/door_lock/models/door_lock_st import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class DoorLockButton extends StatefulWidget { +class DoorLockButton extends StatelessWidget { const DoorLockButton({ super.key, required this.doorLock, @@ -18,70 +18,28 @@ class DoorLockButton extends StatefulWidget { final AllDevicesModel doorLock; final DoorLockStatusModel smartDoorModel; - @override - State createState() => - _DoorLockButtonState(smartDoorModel: smartDoorModel); -} - -class _DoorLockButtonState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _animation; - DoorLockStatusModel smartDoorModel; - - _DoorLockButtonState({required this.smartDoorModel}); - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 500), - ); - _animation = Tween(begin: 0, end: 1).animate(_animationController) - ..addListener(() { - setState(() {}); - }); - - if (smartDoorModel.unlockRequest > 0) { - _animationController.reverse(from: 1); - } - } - - @override - void didUpdateWidget(covariant DoorLockButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.smartDoorModel.normalOpenSwitch != - widget.smartDoorModel.normalOpenSwitch) { - setState(() { - smartDoorModel = widget.smartDoorModel; - }); - - if (smartDoorModel.unlockRequest > 0) { - _animationController.forward(from: 0); - } else { - _animationController.reverse(from: 1); - } - } - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); + double _calculateProgress() { + final value = smartDoorModel.unlockRequest; + if (value <= 0 || value > 30) return 0; + return value / 30.0; } @override Widget build(BuildContext context) { + final progress = _calculateProgress(); + final isEnabled = smartDoorModel.unlockRequest > 0; + return SizedBox( width: 255, height: 255, child: InkWell( - onTap: () { - _animationController.forward(from: 0); - BlocProvider.of(context) - .add(UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch)); - }, + onTap: isEnabled + ? () { + BlocProvider.of(context).add( + UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch), + ); + } + : null, child: Container( width: 255, height: 255, @@ -115,15 +73,16 @@ class _DoorLockButtonState extends State ), ), ), - SizedBox.expand( - child: CircularProgressIndicator( - value: _animation.value, - strokeWidth: 8, - backgroundColor: Colors.transparent, - valueColor: const AlwaysStoppedAnimation( - ColorsManager.primaryColor), + if (progress > 0) + SizedBox.expand( + child: CircularProgressIndicator( + value: progress, + strokeWidth: 8, + backgroundColor: Colors.transparent, + valueColor: const AlwaysStoppedAnimation( + ColorsManager.primaryColor), + ), ), - ), ], ), ), From 28ac911f3f6f939b3e20dc140f56dd012086f1e0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 15:30:47 +0300 Subject: [PATCH 030/105] Accomodated for null values in `SpaceModel`. --- .../communities/domain/models/space_model.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index d6007815..36943adb 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -2,8 +2,8 @@ import 'package:equatable/equatable.dart'; class SpaceModel extends Equatable { final String uuid; - final DateTime createdAt; - final DateTime updatedAt; + final DateTime? createdAt; + final DateTime? updatedAt; final String spaceName; final String icon; final List children; @@ -21,11 +21,11 @@ class SpaceModel extends Equatable { factory SpaceModel.fromJson(Map json) { return SpaceModel( - uuid: json['uuid'] as String, - createdAt: DateTime.parse(json['createdAt'] as String), - updatedAt: DateTime.parse(json['updatedAt'] as String), - spaceName: json['spaceName'] as String, - icon: json['icon'] as String, + uuid: json['uuid'] as String? ?? '', + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''), + updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''), + spaceName: json['spaceName'] as String? ?? '', + icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg', children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? From 48d7ab430f524e242c2f46523da1f92c65e773e3 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 22 Jun 2025 15:35:46 +0300 Subject: [PATCH 031/105] refactor: rename productName to deviceNameOrProductName in search functionality --- .../device_managment_bloc.dart | 72 ++++++++++--------- .../device_managment_event.dart | 6 +- .../widgets/device_search_filters.dart | 4 +- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 05e82f1f..98b0c195 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -40,17 +40,18 @@ class DeviceManagementBloc List devices = []; _devices.clear(); var spaceBloc = event.context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (spaceBloc.state.selectedCommunities.isEmpty) { - devices = await DevicesManagementApi().fetchDevices('', '', projectUuid); + devices = + await DevicesManagementApi().fetchDevices('', '', projectUuid); } else { for (var community in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; for (var space in spacesList) { - devices.addAll(await DevicesManagementApi().fetchDevices( - community, space, projectUuid)); + devices.addAll(await DevicesManagementApi() + .fetchDevices(community, space, projectUuid)); } } } @@ -100,7 +101,7 @@ class DeviceManagementBloc )); if (currentProductName.isNotEmpty) { - add(SearchDevices(productName: currentProductName)); + add(SearchDevices(deviceNameOrProductName: currentProductName)); } } } @@ -269,34 +270,41 @@ class DeviceManagementBloc return 'All'; } } - void _onSearchDevices( SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) && - (event.productName == null || event.productName!.isEmpty)) { + (event.deviceNameOrProductName == null || + event.deviceNameOrProductName!.isEmpty)) { currentProductName = ''; - if (state is DeviceManagementFiltered) { - add(FilterDevices(_getFilterFromIndex(_selectedIndex))); - } else { - return; - } + _filteredDevices = List.from(_devices); + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: _selectedIndex, + onlineCount: _onlineCount, + offlineCount: _offlineCount, + lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, + )); + return; } - - if (event.productName == currentProductName && + if (event.deviceNameOrProductName == currentProductName && event.community == currentCommunity && event.unitName == currentUnitName && event.searchField) { return; } - currentProductName = event.productName ?? ''; + currentProductName = event.deviceNameOrProductName ?? ''; currentCommunity = event.community; currentUnitName = event.unitName; - List devicesToSearch = _filteredDevices; + List devicesToSearch = _devices; if (devicesToSearch.isNotEmpty) { + final searchText = event.deviceNameOrProductName?.toLowerCase() ?? ''; + final filteredDevices = devicesToSearch.where((device) { final matchesCommunity = event.community == null || event.community!.isEmpty || @@ -304,31 +312,25 @@ class DeviceManagementBloc ?.toLowerCase() .contains(event.community!.toLowerCase()) ?? false); + final matchesUnit = event.unitName == null || event.unitName!.isEmpty || (device.spaces != null && - device.spaces!.isNotEmpty && - device.spaces![0].spaceName! - .toLowerCase() - .contains(event.unitName!.toLowerCase())); - final matchesProductName = event.productName == null || - event.productName!.isEmpty || - (device.name - ?.toLowerCase() - .contains(event.productName!.toLowerCase()) ?? - false); - final matchesDeviceName = event.productName == null || - event.productName!.isEmpty || - (device.categoryName - ?.toLowerCase() - .contains(event.productName!.toLowerCase()) ?? - false); + device.spaces!.any((space) => + space.spaceName != null && + space.spaceName! + .toLowerCase() + .contains(event.unitName!.toLowerCase()))); - return matchesCommunity && - matchesUnit && - (matchesProductName || matchesDeviceName); + final matchesSearchText = searchText.isEmpty || + (device.name?.toLowerCase().contains(searchText) ?? false) || + (device.productName?.toLowerCase().contains(searchText) ?? false); + + return matchesCommunity && matchesUnit && matchesSearchText; }).toList(); + _filteredDevices = filteredDevices; + emit(DeviceManagementFiltered( filteredDevices: filteredDevices, selectedIndex: _selectedIndex, diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart index 9928c50e..5292de0e 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart @@ -38,18 +38,18 @@ class SelectedFilterChanged extends DeviceManagementEvent { class SearchDevices extends DeviceManagementEvent { final String? community; final String? unitName; - final String? productName; + final String? deviceNameOrProductName; final bool searchField; const SearchDevices({ this.community, this.unitName, - this.productName, + this.deviceNameOrProductName, this.searchField = false, }); @override - List get props => [community, unitName, productName]; + List get props => [community, unitName, deviceNameOrProductName]; } class SelectDevice extends DeviceManagementEvent { 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 6440d18f..7e998ed6 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 @@ -53,7 +53,7 @@ class _DeviceSearchFiltersState extends State controller: controller, onSubmitted: () { final searchDevicesEvent = SearchDevices( - productName: _productNameController.text, + deviceNameOrProductName: _productNameController.text, unitName: _unitNameController.text, searchField: true, ); @@ -68,7 +68,7 @@ class _DeviceSearchFiltersState extends State onSearch: () => context.read().add( SearchDevices( unitName: _unitNameController.text, - productName: _productNameController.text, + deviceNameOrProductName: _productNameController.text, searchField: true, ), ), From 41d4fbb555585a3cda5da9221d2a7faa06543638 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 16:00:20 +0300 Subject: [PATCH 032/105] Extracted pagination data into a generic DTO. --- .../shared/models/paginated_data_model.dart | 45 ++++++++++++ .../debounced_communities_service.dart | 1 - .../services/remote_communities_service.dart | 4 +- .../models/communities_pagination_model.dart | 69 ------------------- .../domain/services/communities_service.dart | 5 +- .../presentation/bloc/communities_bloc.dart | 4 +- 6 files changed, 53 insertions(+), 75 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart delete mode 100644 lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart new file mode 100644 index 00000000..e37cd0a1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; + +class PaginatedDataModel extends Equatable { + const PaginatedDataModel({ + required this.data, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List data; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + factory PaginatedDataModel.fromJson( + Map json, + T Function(Map) fromJsonT, + ) { + return PaginatedDataModel( + data: (json['data'] as List? ?? []) + .map((e) => fromJsonT(e as Map)) + .toList(), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, + ); + } + + @override + List get props => [ + data, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index e512679b..a97e8524 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 925a1cd0..b58961a6 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -30,7 +30,7 @@ class RemoteCommunitiesService implements CommunitiesService { }, expectedResponseModel: (json) { final data = json as Map; - return CommunitiesPaginationModel.fromJson(data); + return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); }, ); diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart deleted file mode 100644 index a86783be..00000000 --- a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; - -class CommunitiesPaginationModel extends Equatable { - const CommunitiesPaginationModel({ - required this.communities, - required this.page, - required this.size, - required this.hasNext, - required this.totalItems, - required this.totalPages, - }); - - final List communities; - final int page; - final int size; - final bool hasNext; - final int totalItems; - final int totalPages; - - const CommunitiesPaginationModel.empty() - : communities = const [], - page = 1, - size = 25, - hasNext = false, - totalItems = 0, - totalPages = 0; - - factory CommunitiesPaginationModel.fromJson(Map json) { - return CommunitiesPaginationModel( - communities: (json['data'] as List? ?? []) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), - page: json['page'] as int? ?? 1, - size: json['size'] as int? ?? 25, - hasNext: json['hasNext'] as bool? ?? false, - totalItems: json['totalItem'] as int? ?? 0, - totalPages: json['totalPage'] as int? ?? 0, - ); - } - - CommunitiesPaginationModel copyWith({ - List? communities, - int? page, - int? size, - bool? hasNext, - int? totalItems, - int? totalPages, - }) { - return CommunitiesPaginationModel( - communities: communities ?? this.communities, - page: page ?? this.page, - size: size ?? this.size, - hasNext: hasNext ?? this.hasNext, - totalItems: totalItems ?? this.totalItems, - totalPages: totalPages ?? this.totalPages, - ); - } - - @override - List get props => [ - communities, - page, - size, - hasNext, - totalItems, - totalPages, - ]; -} diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index 564dc4da..baa84590 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,9 @@ -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +typedef CommunitiesPaginationModel = PaginatedDataModel; + abstract class CommunitiesService { Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0f754b06..9094a632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -36,7 +36,7 @@ class CommunitiesBloc extends Bloc { emit( CommunitiesState( status: CommunitiesStatus.success, - communities: paginationResponse.communities, + communities: paginationResponse.data, hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, searchQuery: event.param.search, @@ -67,7 +67,7 @@ class CommunitiesBloc extends Bloc { final paginationResponse = await _communitiesService.getCommunity(param); final updatedCommunities = List.from(state.communities) - ..addAll(paginationResponse.communities); + ..addAll(paginationResponse.data); emit( state.copyWith( From 27349a6cc0956878a0b590f8ffd66e2d7245bc7e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 09:24:53 +0300 Subject: [PATCH 033/105] Implemented PR notes by extracting widgets into their own classes. --- lib/common/widgets/app_loading_indicator.dart | 10 + .../shared/models/paginated_data_model.dart | 6 +- .../services/remote_communities_service.dart | 8 +- .../domain/models/community_model.dart | 5 + .../communities_tree_failure_widget.dart | 38 +++ ...communities_tree_search_result_widget.dart | 22 ++ .../space_management_communities_tree.dart | 240 ++++-------------- ...ement_communities_tree_community_tile.dart | 45 ++++ ...anagement_communities_tree_space_tile.dart | 56 ++++ .../space_management_sidebar_header.dart | 46 +++- .../create_community_dialog_widget.dart | 2 +- 11 files changed, 271 insertions(+), 207 deletions(-) create mode 100644 lib/common/widgets/app_loading_indicator.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart diff --git a/lib/common/widgets/app_loading_indicator.dart b/lib/common/widgets/app_loading_indicator.dart new file mode 100644 index 00000000..bc811c56 --- /dev/null +++ b/lib/common/widgets/app_loading_indicator.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AppLoadingIndicator extends StatelessWidget { + const AppLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart index e37cd0a1..ac35975d 100644 --- a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -19,12 +19,10 @@ class PaginatedDataModel extends Equatable { factory PaginatedDataModel.fromJson( Map json, - T Function(Map) fromJsonT, + List Function(List) fromJsonList, ) { return PaginatedDataModel( - data: (json['data'] as List? ?? []) - .map((e) => fromJsonT(e as Map)) - .toList(), + data: fromJsonList(json['data'] as List), page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 25, hasNext: json['hasNext'] as bool? ?? false, diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index b58961a6..cc842de8 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -28,10 +28,10 @@ class RemoteCommunitiesService implements CommunitiesService { if (param.search.isNotEmpty && param.search != 'null') 'search': param.search, }, - expectedResponseModel: (json) { - final data = json as Map; - return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); - }, + expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson( + json as Map, + CommunityModel.fromJsonList, + ), ); return response; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index 344dbff5..37f131b3 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -33,6 +33,11 @@ class CommunityModel extends Equatable { .toList(), ); } + static List fromJsonList(List json) { + return json + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(); + } @override List get props => [uuid, name, spaces]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart new file mode 100644 index 00000000..cfd32f52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; + +class CommunitiesTreeFailureWidget extends StatelessWidget { + const CommunitiesTreeFailureWidget({super.key, this.errorMessage}); + + final String? errorMessage; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.read().add( + LoadCommunities( + LoadCommunitiesParam( + search: context.read().state.searchQuery, + ), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart new file mode 100644 index 00000000..bfc9e30e --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget { + const EmptyCommunitiesTreeSearchResultWidget({ + required this.searchQuery, + super.key, + }); + + final String searchQuery; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "$searchQuery"', + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index efafdd85..1adf9911 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -31,16 +29,6 @@ class _SpaceManagementCommunitiesTreeState super.initState(); } - bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { - final selectedSpace = - context.read().state.selectedSpace; - final isSpaceSelected = selectedSpace?.uuid == space.uuid; - final anySubSpaceIsSelected = space.children.any( - (child) => _isSpaceOrChildSelected(context, child), - ); - return isSpaceSelected || anySubSpaceIsSelected; - } - void _onSearchChanged(String searchQuery) { context .read() @@ -51,67 +39,32 @@ class _SpaceManagementCommunitiesTreeState context.read().add(const LoadMoreCommunities()); } - static const _width = 300.0; - @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { - return Container( - width: _width, - decoration: subSectionContainerDecoration, - child: Column( - children: [ - SpaceManagementSidebarHeader( - onAddCommunity: () => _onAddCommunity(context), - ), - CustomSearchBar( - onSearchChanged: _onSearchChanged, - ), - const SizedBox(height: 16), - switch (state.status) { - CommunitiesStatus.initial => - const Center(child: CircularProgressIndicator()), - CommunitiesStatus.loading => state.communities.isEmpty - ? const Center(child: CircularProgressIndicator()) - : _buildCommunitiesTree(context, state), - CommunitiesStatus.success => _buildCommunitiesTree(context, state), - CommunitiesStatus.failure => _buildErrorState(context, state), - }, - Visibility( - visible: state.isLoadingMore, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Center(child: CircularProgressIndicator()), - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildErrorState(BuildContext context, CommunitiesState state) { - return Expanded( - child: Center( + builder: (context, state) => Container( + width: 320, + decoration: subSectionContainerDecoration, child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - state.errorMessage ?? 'Something went wrong', - textAlign: TextAlign.center, + const SpaceManagementSidebarHeader(), + CustomSearchBar( + onSearchChanged: _onSearchChanged, ), const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - context.read().add( - LoadCommunities(LoadCommunitiesParam( - search: state.searchQuery, - )), - ); - }, - child: const Text('Retry'), + switch (state.status) { + CommunitiesStatus.initial => const AppLoadingIndicator(), + CommunitiesStatus.loading => state.communities.isEmpty + ? const AppLoadingIndicator() + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => CommunitiesTreeFailureWidget( + errorMessage: state.errorMessage, + ), + }, + Visibility( + visible: state.isLoadingMore, + child: const AppLoadingIndicator(), ), ], ), @@ -123,132 +76,37 @@ class _SpaceManagementCommunitiesTreeState BuildContext context, CommunitiesState state, ) { - if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { - return Expanded( - child: Center( - child: Text( - state.searchQuery.isEmpty - ? 'No communities found' - : 'No communities found for "${state.searchQuery}"', - textAlign: TextAlign.center, - ), - ), - ); - } + final communitiesIsEmpty = state.communities.isEmpty; + final statusIsSuccess = state.status == CommunitiesStatus.success; return Expanded( - child: Stack( - children: [ - SpaceManagementSidebarCommunitiesList( - communities: state.communities, - onLoadMore: state.hasNext ? _onLoadMore : null, - isLoadingMore: state.isLoadingMore, - hasNext: state.hasNext, - itemBuilder: (context, index) { - return _buildCommunityTile(context, state.communities[index]); - }, - ), - if (state.status == CommunitiesStatus.loading && - state.communities.isNotEmpty) - ColoredBox( - color: Colors.white.withValues(alpha: 0.7), - child: const Center(child: CircularProgressIndicator()), + child: Visibility( + visible: statusIsSuccess && communitiesIsEmpty, + replacement: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return SpaceManagementCommunitiesTreeCommunityTile( + community: state.communities[index], + ); + }, ), - ], - ), - ); - } - - Widget _buildCommunityTile(BuildContext context, CommunityModel community) { - final spaces = community.spaces - .map((space) => _buildSpaceTile( - space: space, - community: community, - context: context, - )) - .toList(); - return CommunityTile( - title: community.name, - key: ValueKey(community.uuid), - isSelected: context - .watch() - .state - .selectedCommunity - ?.uuid == - community.uuid, - isExpanded: false, - onItemSelected: () { - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - onExpansionChanged: (title, expanded) {}, - children: spaces, - ); - } - - Widget _buildSpaceTile({ - required SpaceModel space, - required CommunityModel community, - required BuildContext context, - }) { - final spaceIsExpanded = _isSpaceOrChildSelected(context, space); - final isSelected = - context.watch().state.selectedSpace?.uuid == - space.uuid; - return Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: SpaceTile( - title: space.spaceName, - key: ValueKey(space.uuid), - isSelected: isSelected, - initiallyExpanded: spaceIsExpanded, - onExpansionChanged: (expanded) {}, - onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), - ), - children: space.children - .map( - (childSpace) => _buildSpaceTile( - space: childSpace, - community: community, - context: context, + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const AppLoadingIndicator(), ), - ) - .toList(), + ], + ), + child: EmptyCommunitiesTreeSearchResultWidget( + searchQuery: state.searchQuery, + ), ), ); } - - void _onAddCommunity(BuildContext context) { - context - .read() - .state - .selectedCommunity - ?.uuid - .isNotEmpty ?? - false - ? _clearSelection(context) - : _showCreateCommunityDialog(context); - } - - void _clearSelection(BuildContext context) => - context.read().add( - const ClearCommunitiesTreeSelectionEvent(), - ); - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart new file mode 100644 index 00000000..736a499f --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart'; + +class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeCommunityTile({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaces = community.spaces + .map( + (space) => SpaceManagementCommunitiesTreeSpaceTile( + space: space, + community: community, + ), + ) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart new file mode 100644 index 00000000..dcd44ac8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; + +class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeSpaceTile({ + required this.space, + required this.community, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => SpaceManagementCommunitiesTreeSpaceTile( + space: childSpace, + community: community, + ), + ) + .toList(), + ), + ); + } + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index cf40f95c..25c094db 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementSidebarHeader extends StatelessWidget { - const SpaceManagementSidebarHeader({ - required this.onAddCommunity, - super.key, - }); - - final void Function() onAddCommunity; + const SpaceManagementSidebarHeader({super.key}); @override Widget build(BuildContext context) { @@ -27,10 +26,43 @@ class SpaceManagementSidebarHeader extends StatelessWidget { ), ), SpaceManagementSidebarAddCommunityButton( - onTap: onAddCommunity, + onTap: () => _onAddCommunity(context), ), ], ), ); } + + void _onAddCommunity(BuildContext context) { + final bloc = context.read(); + final selectedCommunity = bloc.state.selectedCommunity; + final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false; + + if (isSelected) { + _clearSelection(context); + } else { + _showCreateCommunityDialog(context); + } + } + + void _clearSelection(BuildContext context) { + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + } + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); } diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart index 49d43ae6..ab9f7b9a 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -99,7 +99,7 @@ class _CreateCommunityDialogWidgetState extends State Date: Mon, 23 Jun 2025 10:01:01 +0300 Subject: [PATCH 034/105] Refactor empty state widget to use a container for better layout control --- lib/pages/common/custom_table.dart | 55 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 625c59c2..fb8237b7 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -179,31 +179,36 @@ class _DynamicTableState extends State { ); } - Widget _buildEmptyState() => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - SvgPicture.asset(Assets.emptyTable), - const SizedBox(height: 15), - Text( - widget.tableName == 'AccessManagement' - ? 'No Password ' - : 'No Devices', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: ColorsManager.grayColor), - ) - ], - ), - ], - ), - ], + Widget _buildEmptyState() => Container( + height: widget.size.height, + color: ColorsManager.whiteColors, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox(height: 15), + Text( + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor), + ) + ], + ), + ], + ), + SizedBox(height: widget.size.height * 0.5), + ], + ), ); Widget _buildSelectAllCheckbox() { return Container( From ff3d5cd996d4493d2bf89dbc125c45b074d6528f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:02 +0300 Subject: [PATCH 035/105] Created a helper class to show create community dialog, since this dialog can be shown from two different widgets. --- ...ce_management_community_dialog_helper.dart | 24 +++++++++++++++++++ .../space_management_sidebar_header.dart | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart new file mode 100644 index 00000000..a18834df --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; + +abstract final class SpaceManagementCommunityDialogHelper { + static void showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index 25c094db..b5f2a1b7 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget { if (isSelected) { _clearSelection(context); } else { - _showCreateCommunityDialog(context); + SpaceManagementCommunityDialogHelper.showCreateDialog(context); } } @@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget { const ClearCommunitiesTreeSelectionEvent(), ); } - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } From 0e7109a19e297bb461a8214d01c504d5b0ab6098 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:15 +0300 Subject: [PATCH 036/105] Created `CommunityTemplateCell` widget. --- .../widgets/community_template_cell.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart new file mode 100644 index 00000000..4352d069 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityTemplateCell extends StatelessWidget { + const CommunityTemplateCell({ + super.key, + required this.onTap, + required this.title, + }); + + final void Function() onTap; + final Widget title; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 2.0, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 4, + strokeAlign: BorderSide.strokeAlignOutside, + color: ColorsManager.borderColor, + ), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), + ), + DefaultTextStyle( + style: context.textTheme.bodyLarge!.copyWith( + color: ColorsManager.blackColor, + ), + child: title, + ), + ], + ), + ); + } +} From a78b5993a9aa6c7e425474c0c23f30d3b3c2cadd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:05:53 +0300 Subject: [PATCH 037/105] Created `SpaceManagementTemplatesView` widget. --- .../space_management_templates_view.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart new file mode 100644 index 00000000..dd46d2c1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceManagementTemplatesView extends StatelessWidget { + const SpaceManagementTemplatesView({super.key}); + @override + Widget build(BuildContext context) { + return Expanded( + child: ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, + ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, + ), + ), + ); + } + + List<_CommunityTemplateModel> _gridItems(BuildContext context) { + return [ + _CommunityTemplateModel( + title: const Text('Blank'), + onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context), + ), + ]; + } +} + +class _CommunityTemplateModel { + final Widget title; + final void Function() onTap; + + _CommunityTemplateModel({ + required this.title, + required this.onTap, + }); +} From 7d4cdba0eff5c9e09ac075f98302ea9a8857c173 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:06:59 +0300 Subject: [PATCH 038/105] Connected templates view into `SpaceManagementBody`, while applying the correct UI principals if what to show what when? --- .../widgets/space_management_body.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 3a9aa3c8..5d28a533 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; class SpaceManagementBody extends StatelessWidget { @@ -6,9 +9,21 @@ class SpaceManagementBody extends StatelessWidget { @override Widget build(BuildContext context) { - return const Row( + return Row( children: [ - SpaceManagementCommunitiesTree(), + const SpaceManagementCommunitiesTree(), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.selectedCommunity != current.selectedCommunity, + builder: (context, state) => Visibility( + visible: state.selectedCommunity == null, + replacement: const Placeholder(), + child: const SpaceManagementTemplatesView(), + ), + ), + ), ], ); } From f8e4c89cdb63e50ef6cc1323a298f603690f36cf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:03 +0300 Subject: [PATCH 039/105] uses correct error message that the api sends in `RemoteCreateCommunityService`. --- .../data/services/remote_create_community_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index bd91f6ce..aae92e9f 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService { return _defaultErrorMessage; } final error = body['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; return errorMessage; } From 4bdb487094fc81d9786acaacf2fcceccc8ee6f2b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:23 +0300 Subject: [PATCH 040/105] doesnt show a snackbar when creating a community fails, since we show the error message in the dialog itself. --- .../presentation/create_community_dialog.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index 8c1d474d..a9af44d6 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget { ); onCreateCommunity.call(community); break; - case CreateCommunityFailure(:final message): + case CreateCommunityFailure(): Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); break; default: break; From ada7daf17950efb2aba67d39efd867d796f2facd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:13:30 +0300 Subject: [PATCH 041/105] Switched from using `Text` to `SelectableText` in `CreateCommunityDialog`. --- .../helpers/space_management_community_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart index a18834df..5322c3ea 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -9,7 +9,7 @@ abstract final class SpaceManagementCommunityDialogHelper { showDialog( context: context, builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), + title: const SelectableText('Community Name'), onCreateCommunity: (community) { context.read().add( InsertCommunity(community), From 1200a809c2719f9da1f4c6d6e86b51d468981d45 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 14:33:56 +0300 Subject: [PATCH 042/105] now cant use offline device to controll --- .../widgets/device_managment_body.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index f4baad0c..c484b1b1 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -103,8 +103,30 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { decoration: containerDecoration, child: Center( child: DefaultButton( + backgroundColor: selectedDevices.any( + (element) => !element.online!, + ) + ? ColorsManager.primaryColor + .withValues(alpha: 0.1) + : null, onPressed: isControlButtonEnabled ? () { + if (selectedDevices.any( + (element) => !element.online!, + )) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'This Device is Offline', + ), + duration: + Duration(seconds: 2), + ), + ); + return; + } + if (selectedDevices.length == 1) { showDialog( context: context, From 71cf0a636e15c2009bd9f134ef158e685f07a9c0 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:22:24 +0300 Subject: [PATCH 043/105] send all user instead of only uuid --- .../users_table/view/users_page.dart | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 767fd9a6..88f89e30 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -19,6 +19,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; + class UsersPage extends StatelessWidget { UsersPage({super.key}); @@ -451,33 +452,33 @@ class UsersPage extends StatelessWidget { ), Row( children: [ - user.isEnabled != false - ? actionButton( - isActive: true, - title: "Edit", - onTap: () { - context - .read() - .add(ClearCachedData()); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EditUserDialog( - userId: user.uuid); - }, - ).then((v) { - if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } - } - }); + if (user.isEnabled != false) + actionButton( + isActive: true, + title: "Edit", + onTap: () { + context + .read() + .add(ClearCachedData()); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog(user: user); }, - ) - : actionButton( - title: "Edit", - ), + ).then((v) { + if (v != null) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + } + }); + }, + ) + else + actionButton( + title: "Edit", + ), actionButton( title: "Delete", onTap: () { From e6957d566da8ec3e4a24dc9e23ffe7d47eebb290 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:29:35 +0300 Subject: [PATCH 044/105] use RoleUserModel instead of only id and all is good --- .../view/edit_user_dialog.dart | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index 071de067..c84a6baf 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; @@ -12,8 +14,11 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class EditUserDialog extends StatefulWidget { - final String? userId; - const EditUserDialog({super.key, this.userId}); + final RolesUserModel? user; + const EditUserDialog({ + super.key, + this.user, + }); @override _EditUserDialogState createState() => _EditUserDialogState(); @@ -28,10 +33,11 @@ class _EditUserDialogState extends State { create: (BuildContext context) => UsersBloc() // ..add(const LoadCommunityAndSpacesEvent()) ..add(const RoleEvent()) - ..add(GetUserByIdEvent(uuid: widget.userId)), + ..add(GetUserByIdEvent(uuid: widget.user!.uuid)), child: BlocConsumer(listener: (context, state) { if (state is SpacesLoadedState) { - BlocProvider.of(context).add(GetUserByIdEvent(uuid: widget.userId)); + BlocProvider.of(context) + .add(GetUserByIdEvent(uuid: widget.user!.uuid)); } }, builder: (context, state) { final _blocRole = BlocProvider.of(context); @@ -39,7 +45,8 @@ class _EditUserDialogState extends State { return Dialog( child: Container( decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), width: 900, child: Column( children: [ @@ -68,7 +75,8 @@ class _EditUserDialogState extends State { children: [ _buildStep1Indicator(1, "Basics", _blocRole), _buildStep2Indicator(2, "Spaces", _blocRole), - _buildStep3Indicator(3, "Role & Permissions", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), ], ), ), @@ -86,7 +94,7 @@ class _EditUserDialogState extends State { children: [ const SizedBox(height: 10), Expanded( - child: _getFormContent(widget.userId), + child: _getFormContent(widget.user!), ), const SizedBox(height: 20), ], @@ -116,13 +124,14 @@ class _EditUserDialogState extends State { if (currentStep < 3) { currentStep++; if (currentStep == 2) { - _blocRole.add(CheckStepStatus(isEditUser: true)); + _blocRole + .add(CheckStepStatus(isEditUser: true)); } else if (currentStep == 3) { _blocRole.add(const CheckSpacesStepStatus()); } } else { - _blocRole - .add(EditInviteUsers(context: context, userId: widget.userId!)); + _blocRole.add(EditInviteUsers( + context: context, userId: widget.user!.uuid)); } }); }, @@ -131,7 +140,8 @@ class _EditUserDialogState extends State { style: TextStyle( color: (_blocRole.isCompleteSpaces == false || _blocRole.isCompleteBasics == false || - _blocRole.isCompleteRolePermissions == false) && + _blocRole.isCompleteRolePermissions == + false) && currentStep == 3 ? ColorsManager.grayColor : ColorsManager.secondaryColor), @@ -146,15 +156,15 @@ class _EditUserDialogState extends State { })); } - Widget _getFormContent(userid) { + Widget _getFormContent(RolesUserModel user) { switch (currentStep) { case 1: return BasicsView( - userId: userid, + userId: user.uuid, ); case 2: return SpacesAccessView( - userId: userid, + userId: user.uuid, ); case 3: return const RolesAndPermission(); @@ -204,8 +214,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -263,8 +277,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -321,8 +339,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], From 8bc7a3daa2b6cc3fdd44767186f6536ad6b8a0af Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 15:45:49 +0300 Subject: [PATCH 045/105] Implemented space management canvas. --- .../models/space_connection_model.dart | 6 + .../spaces_connections_arrow_painter.dart | 60 +++++ .../widgets/community_structure_canvas.dart | 236 ++++++++++++++++++ .../widgets/create_space_button.dart | 42 ++++ .../widgets/plus_button_widget.dart | 43 ++++ .../widgets/space_card_widget.dart | 65 +++++ .../main_module/widgets/space_cell.dart | 88 +++++++ .../widgets/space_management_body.dart | 3 +- .../space_management_community_structure.dart | 22 ++ .../communities_tree_selection_bloc.dart | 2 +- .../communities_tree_selection_event.dart | 7 +- ...anagement_communities_tree_space_tile.dart | 2 +- lib/utils/app_routes.dart | 2 +- 13 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/models/space_connection_model.dart create mode 100644 lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/create_space_button.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_cell.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart diff --git a/lib/pages/space_management_v2/main_module/models/space_connection_model.dart b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart new file mode 100644 index 00000000..538a922c --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart @@ -0,0 +1,6 @@ +class SpaceConnectionModel { + final String from; + final String to; + + const SpaceConnectionModel({required this.from, required this.to}); +} diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart new file mode 100644 index 00000000..fcf523bf --- /dev/null +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesConnectionsArrowPainter extends CustomPainter { + final List connections; + final Map positions; + final double cardWidth = 150.0; + final double cardHeight = 90.0; + final String? selectedSpaceUuid; + + SpacesConnectionsArrowPainter({ + required this.connections, + required this.positions, + this.selectedSpaceUuid, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final connection in connections) { + final isSelected = connection.to == selectedSpaceUuid; + final paint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + final from = positions[connection.from]; + final to = positions[connection.to]; + + if (from != null && to != null) { + final startPoint = + Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); + final endPoint = Offset(to.dx + cardWidth / 2, to.dy); + + final path = Path()..moveTo(startPoint.dx, startPoint.dy); + + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); + + path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, + controlPoint2.dy, endPoint.dx, endPoint.dy); + + canvas.drawPath(path, paint); + + final circlePaint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.srcIn; + canvas.drawCircle(endPoint, 4, circlePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart new file mode 100644 index 00000000..92c5add6 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class CommunityStructureCanvas extends StatefulWidget { + const CommunityStructureCanvas({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + State createState() =>_CommunityStructureCanvasState(); +} + +class _CommunityStructureCanvasState extends State + with SingleTickerProviderStateMixin { + final Map _positions = {}; + final double _cardWidth = 150.0; + final double _cardHeight = 90.0; + final double _horizontalSpacing = 150.0; + final double _verticalSpacing = 120.0; + String? _selectedSpaceUuid; + + late TransformationController _transformationController; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _transformationController = TransformationController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + } + + @override + void dispose() { + _transformationController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _runAnimation(Matrix4 target) { + final animation = Matrix4Tween( + begin: _transformationController.value, + end: target, + ).animate(_animationController); + + void listener() { + _transformationController.value = animation.value; + } + + animation.addListener(listener); + _animationController.forward(from: 0).whenCompleteOrCancel(() { + animation.removeListener(listener); + }); + } + + void _onSpaceTapped(String spaceUuid) { + setState(() { + _selectedSpaceUuid = spaceUuid; + }); + + final position = _positions[spaceUuid]; + if (position == null) return; + + const scale = 2.0; + final viewSize = context.size; + if (viewSize == null) return; + + final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); + final y = + -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); + + final matrix = Matrix4.identity() + ..translate(x, y) + ..scale(scale); + + _runAnimation(matrix); + } + + void _resetSelectionAndZoom() { + setState(() { + _selectedSpaceUuid = null; + }); + _runAnimation(Matrix4.identity()); + } + + void _calculateLayout( + List spaces, + int depth, + Map levelXOffset, + ) { + for (final space in spaces) { + double childSubtreeWidth = 0; + if (space.children.isNotEmpty) { + _calculateLayout(space.children, depth + 1, levelXOffset); + final firstChildPos = _positions[space.children.first.uuid]; + final lastChildPos = _positions[space.children.last.uuid]; + if (firstChildPos != null && lastChildPos != null) { + childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; + } + } + + final currentX = levelXOffset.putIfAbsent(depth, () => 0.0); + double? x; + + if (space.children.isNotEmpty) { + final firstChildPos = _positions[space.children.first.uuid]!; + x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; + } else { + x = currentX; + } + + if (x < currentX) { + final shiftX = currentX - x; + _shiftSubtree(space, shiftX); + final keysToShift = levelXOffset.keys.where((d) => d > depth).toList(); + for (final key in keysToShift) { + levelXOffset[key] = levelXOffset[key]! + shiftX; + } + x += shiftX; + } + + final y = depth * (_verticalSpacing + _cardHeight); + _positions[space.uuid] = Offset(x, y); + levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; + } + } + + void _shiftSubtree(SpaceModel space, double shiftX) { + if (_positions.containsKey(space.uuid)) { + _positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0); + } + for (final child in space.children) { + _shiftSubtree(child, shiftX); + } + } + + List _buildTreeWidgets() { + _positions.clear(); + final community = widget.community; + + _calculateLayout(community.spaces, 0, {}); + + final widgets = []; + final connections = []; + _generateWidgets(community.spaces, widgets, connections); + + return [ + CustomPaint( + painter: SpacesConnectionsArrowPainter( + connections: connections, + positions: _positions, + selectedSpaceUuid: _selectedSpaceUuid, + ), + child: Stack(alignment: AlignmentDirectional.center, children: widgets), + ), + ]; + } + + void _generateWidgets( + List spaces, + List widgets, + List connections, + ) { + for (final space in spaces) { + final position = _positions[space.uuid]; + if (position == null) continue; + + widgets.add( + Positioned( + left: position.dx, + top: position.dy, + width: _cardWidth, + height: _cardHeight, + child: SpaceCardWidget( + index: spaces.indexOf(space), + onPositionChanged: (newPosition) {}, + buildSpaceContainer: (index) { + return Opacity( + opacity: 1.0, + child: SpaceCell( + index: index, + onTap: () => _onSpaceTapped(space.uuid), + icon: space.icon, + name: space.spaceName, + ), + ); + }, + screenSize: MediaQuery.sizeOf(context), + position: position, + isHovered: false, + onHoverChanged: (int index, bool isHovered) {}, + onButtonTap: (int index, Offset newPosition) {}, + ), + ), + ); + + for (final child in space.children) { + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + _generateWidgets(space.children, widgets, connections); + } + } + + @override + Widget build(BuildContext context) { + final treeWidgets = _buildTreeWidgets(); + return InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: MediaQuery.sizeOf(context).width * 0.3, + vertical: MediaQuery.sizeOf(context).height * 0.2, + ), + minScale: 0.5, + maxScale: 3.0, + constrained: false, + child: GestureDetector( + onTap: _resetSelectionAndZoom, + child: SizedBox( + width: MediaQuery.sizeOf(context).width * 2, + height: MediaQuery.sizeOf(context).height * 2, + child: Stack(children: treeWidgets), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart new file mode 100644 index 00000000..5caf6a81 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSpaceButton extends StatelessWidget { + const CreateSpaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () {}, + child: Container( + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart new file mode 100644 index 00000000..755a6ab9 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PlusButtonWidget extends StatelessWidget { + final int index; + final String direction; + final Offset offset; + final void Function(int index, Offset newPosition) onButtonTap; + + const PlusButtonWidget({ + super.key, + required this.index, + required this.direction, + required this.offset, + required this.onButtonTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (direction == 'down') { + onButtonTap(index, const Offset(0, 150)); + } else { + onButtonTap(index, const Offset(150, 0)); + } + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart new file mode 100644 index 00000000..1ce28502 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; + +class SpaceCardWidget extends StatelessWidget { + final int index; + final Size screenSize; + final Offset position; + final bool isHovered; + final void Function(int index, bool isHovered) onHoverChanged; + final void Function(int index, Offset newPosition) onButtonTap; + final Widget Function(int index) buildSpaceContainer; + final ValueChanged onPositionChanged; + + const SpaceCardWidget({ + super.key, + required this.index, + required this.onPositionChanged, + required this.screenSize, + required this.position, + required this.isHovered, + required this.onHoverChanged, + required this.onButtonTap, + required this.buildSpaceContainer, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => onHoverChanged(index, true), + onExit: (_) => onHoverChanged(index, false), + child: SizedBox( + width: 150, + height: 90, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + buildSpaceContainer(index), + + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + index: index, + direction: 'down', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + if (isHovered) + Positioned( + right: -15, + child: PlusButtonWidget( + index: index, + direction: 'right', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart new file mode 100644 index 00000000..1b08835a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceCell extends StatelessWidget { + final int index; + final String icon; + final String name; + final VoidCallback? onDoubleTap; + final VoidCallback? onTap; + + const SpaceCell({ + super.key, + required this.index, + required this.icon, + required this.name, + this.onTap, + this.onDoubleTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GestureDetector( + onDoubleTap: onDoubleTap, + onTap: onTap, + child: Container( + width: 150, + height: 70, + decoration: _containerDecoration(), + child: Row( + children: [ + _buildIconContainer(), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } + + Widget _buildIconContainer() { + return Container( + width: 40, + height: double.infinity, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + color: ColorsManager.whiteColors, + width: 24, + height: 24, + ), + ), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: ColorsManager.lightGrayColor.withValues(alpha: 0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 5d28a533..5d81bffb 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; @@ -19,7 +20,7 @@ class SpaceManagementBody extends StatelessWidget { previous.selectedCommunity != current.selectedCommunity, builder: (context, state) => Visibility( visible: state.selectedCommunity == null, - replacement: const Placeholder(), + replacement: const SpaceManagementCommunityStructure(), child: const SpaceManagementTemplatesView(), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart new file mode 100644 index 00000000..11ee5078 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; + +class SpaceManagementCommunityStructure extends StatelessWidget { + const SpaceManagementCommunityStructure({super.key}); + + @override + Widget build(BuildContext context) { + final selectedCommunity = + context.watch().state.selectedCommunity!; + const spacer = Spacer(flex: 10); + return Visibility( + visible: selectedCommunity.spaces.isNotEmpty, + replacement: const Row( + children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + child: CommunityStructureCanvas(community: selectedCommunity), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart index bfc02f11..bdda04ee 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc ) { emit( CommunitiesTreeSelectionState( - selectedCommunity: null, + selectedCommunity: event.community, selectedSpace: event.space, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 95ffe173..40a41f74 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable { } final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { - final CommunityModel? community; + final CommunityModel community; const SelectCommunityEvent({required this.community}); @override @@ -16,9 +16,10 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel? space; + final SpaceModel space; + final CommunityModel community; - const SelectSpaceEvent({required this.space}); + const SelectSpaceEvent({required this.space, required this.community}); @override List get props => [space]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart index dcd44ac8..795e2c3a 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { initiallyExpanded: spaceIsExpanded, onExpansionChanged: (expanded) {}, onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), + SelectSpaceEvent(community: community, space: space), ), children: space.children .map( diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 5276f4186c46022f5bf42cc6e9aa23fba8cf8ddb Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:55:56 +0300 Subject: [PATCH 046/105] emit error state when catch error and send the real API exception --- lib/pages/auth/bloc/auth_bloc.dart | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 58950089..bfe0b3eb 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -36,7 +36,8 @@ class AuthBloc extends Bloc { ////////////////////////////// forget password ////////////////////////////////// final TextEditingController forgetEmailController = TextEditingController(); - final TextEditingController forgetPasswordController = TextEditingController(); + final TextEditingController forgetPasswordController = + TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); final forgetEmailKey = GlobalKey(); @@ -53,7 +54,8 @@ class AuthBloc extends Bloc { return; } _remainingTime = 1; - add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false)); + add(UpdateTimerEvent( + remainingTime: _remainingTime, isButtonEnabled: false)); try { forgetEmailValidate = ''; _remainingTime = (await AuthenticationAPI.sendOtp( @@ -90,7 +92,8 @@ class AuthBloc extends Bloc { _timer?.cancel(); add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true)); } else { - add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false)); + add(UpdateTimerEvent( + remainingTime: _remainingTime, isButtonEnabled: false)); } }); } @@ -100,7 +103,7 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } -Future changePassword( + Future changePassword( ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { @@ -122,7 +125,6 @@ Future changePassword( } } - String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -131,7 +133,9 @@ Future changePassword( } void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) { - emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime)); + emit(TimerState( + isButtonEnabled: event.isButtonEnabled, + remainingTime: event.remainingTime)); } ///////////////////////////////////// login ///////////////////////////////////// @@ -151,7 +155,6 @@ Future changePassword( static UserModel? user; bool showValidationMessage = false; - void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -170,11 +173,11 @@ Future changePassword( ); } on APIException catch (e) { validate = e.message; - emit(LoginInitial()); + emit(LoginFailure(error: validate)); return; } catch (e) { validate = 'Something went wrong'; - emit(LoginInitial()); + emit(LoginFailure(error: validate)); return; } @@ -197,7 +200,6 @@ Future changePassword( } } - checkBoxToggle( CheckBoxEvent event, Emitter emit, @@ -339,12 +341,14 @@ Future changePassword( static Future getTokenAndValidate() async { try { const storage = FlutterSecureStorage(); - final firstLaunch = - await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; + final firstLaunch = await SharedPreferencesHelper.readBoolFromSP( + StringsManager.firstLaunch) ?? + true; if (firstLaunch) { storage.deleteAll(); } - await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false); + await SharedPreferencesHelper.saveBoolToSP( + StringsManager.firstLaunch, false); final value = await storage.read(key: Token.loginAccessTokenKey) ?? ''; if (value.isEmpty) { return 'Token not found'; @@ -397,7 +401,9 @@ Future changePassword( final String formattedTime = [ if (days > 0) '${days}d', // Append 'd' for days if (days > 0 || hours > 0) - hours.toString().padLeft(2, '0'), // Show hours if there are days or hours + hours + .toString() + .padLeft(2, '0'), // Show hours if there are days or hours minutes.toString().padLeft(2, '0'), seconds.toString().padLeft(2, '0'), ].join(':'); From ad00cf35ba65e6a935b4203289dfe6bbe0f8ac29 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:05:16 +0300 Subject: [PATCH 047/105] added the PR notes --- .../all_devices/widgets/device_managment_body.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index c484b1b1..c865a5dc 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -62,7 +62,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; - + final isAnyDeviceOffline = + selectedDevices.any((element) => !(element.online ?? false)); return Row( children: [ Expanded(child: SpaceTreeView( @@ -103,17 +104,13 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { decoration: containerDecoration, child: Center( child: DefaultButton( - backgroundColor: selectedDevices.any( - (element) => !element.online!, - ) + backgroundColor: isAnyDeviceOffline ? ColorsManager.primaryColor .withValues(alpha: 0.1) : null, onPressed: isControlButtonEnabled ? () { - if (selectedDevices.any( - (element) => !element.online!, - )) { + if (isAnyDeviceOffline) { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( From 379ecec789e2339fa3e4e4b07419155384991c41 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:15:05 +0300 Subject: [PATCH 048/105] use isCurrentStep instead of checking with multi variables everyTime --- .../view/edit_user_dialog.dart | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index c84a6baf..00c566c6 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -176,6 +176,7 @@ class _EditUserDialogState extends State { int step3 = 0; Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -199,7 +200,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteBasics == false ? Assets.wrongProcessIcon @@ -214,12 +215,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -243,6 +243,7 @@ class _EditUserDialogState extends State { } Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -262,7 +263,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteSpaces == false ? Assets.wrongProcessIcon @@ -277,12 +278,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -306,6 +306,7 @@ class _EditUserDialogState extends State { } Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -324,7 +325,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteRolePermissions == false ? Assets.wrongProcessIcon @@ -339,12 +340,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], From d14cc785a8f23dda61d274cf8b22d4f08bbb4c90 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:16:34 +0300 Subject: [PATCH 049/105] no need for two condtions inside themselves --- .../users_page/users_table/view/users_page.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 88f89e30..da159d94 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -468,9 +468,7 @@ class UsersPage extends StatelessWidget { }, ).then((v) { if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } + _blocRole.add(const GetUsers()); } }); }, From 75efc595b47b6ef4eec385e57b1dd8f26158ad08 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 16:22:11 +0300 Subject: [PATCH 050/105] reverted to old import to avoid confusion with QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 95d6e1ecda4709a693e39f6101ca95af69207f60 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:33:45 +0300 Subject: [PATCH 051/105] if online go green with online status else red with offline status --- .../shared/device_control_dialog.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index beb3b52c..7a046bea 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -79,6 +79,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { } Widget _buildDeviceInfoSection() { + final isOnlineDevice = device.online != null && device.online!; return Padding( padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50), child: Table( @@ -107,7 +108,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { 'Installation Date and Time:', formatDateTime( DateTime.fromMillisecondsSinceEpoch( - ((device.createTime ?? 0) * 1000), + (device.createTime ?? 0) * 1000, ), ), ), @@ -126,12 +127,16 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), TableRow( children: [ - _buildInfoRow('Status:', 'Online', statusColor: Colors.green), + _buildInfoRow( + 'Status:', + isOnlineDevice ? 'Online' : 'offline', + statusColor: isOnlineDevice ? Colors.green : Colors.red, + ), _buildInfoRow( 'Last Offline Date and Time:', formatDateTime( DateTime.fromMillisecondsSinceEpoch( - ((device.updateTime ?? 0) * 1000), + (device.updateTime ?? 0) * 1000, ), ), ), From 95ae50d12dd97ec616d22f0b14d9ab70c2ea7920 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:16:03 +0300 Subject: [PATCH 052/105] navigates to selected space when changed on sidebar in space management canvas. --- .../widgets/community_structure_canvas.dart | 82 ++++++++++++------- .../widgets/plus_button_widget.dart | 14 +--- .../widgets/space_card_widget.dart | 51 ++++-------- .../main_module/widgets/space_cell.dart | 17 ++-- .../space_management_community_structure.dart | 12 ++- .../communities_tree_selection_event.dart | 2 +- 6 files changed, 86 insertions(+), 92 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 92c5add6..22b4536a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ required this.community, + required this.selectedSpace, super.key, }); final CommunityModel community; + final SpaceModel? selectedSpace; @override - State createState() =>_CommunityStructureCanvasState(); + State createState() => _CommunityStructureCanvasState(); } class _CommunityStructureCanvasState extends State @@ -25,19 +29,30 @@ class _CommunityStructureCanvasState extends State final double _cardHeight = 90.0; final double _horizontalSpacing = 150.0; final double _verticalSpacing = 120.0; - String? _selectedSpaceUuid; late TransformationController _transformationController; late AnimationController _animationController; @override void initState() { - super.initState(); _transformationController = TransformationController(); _animationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 100), + duration: const Duration(milliseconds: 150), ); + super.initState(); + } + + @override + void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _animateToSpace(widget.selectedSpace); + } + }); + } } @override @@ -63,15 +78,16 @@ class _CommunityStructureCanvasState extends State }); } - void _onSpaceTapped(String spaceUuid) { - setState(() { - _selectedSpaceUuid = spaceUuid; - }); + void _animateToSpace(SpaceModel? space) { + if (space == null) { + _runAnimation(Matrix4.identity()); + return; + } - final position = _positions[spaceUuid]; + final position = _positions[space.uuid]; if (position == null) return; - const scale = 2.0; + const scale = 1.5; final viewSize = context.size; if (viewSize == null) return; @@ -86,11 +102,19 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _onSpaceTapped(SpaceModel? space) { + context.read().add( + SelectSpaceEvent(community: widget.community, space: space), + ); + } + void _resetSelectionAndZoom() { - setState(() { - _selectedSpaceUuid = null; - }); - _runAnimation(Matrix4.identity()); + context.read().add( + SelectSpaceEvent( + community: widget.community, + space: null, + ), + ); } void _calculateLayout( @@ -159,7 +183,7 @@ class _CommunityStructureCanvasState extends State painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, - selectedSpaceUuid: _selectedSpaceUuid, + selectedSpaceUuid: widget.selectedSpace?.uuid, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), ), @@ -182,24 +206,24 @@ class _CommunityStructureCanvasState extends State width: _cardWidth, height: _cardHeight, child: SpaceCardWidget( - index: spaces.indexOf(space), - onPositionChanged: (newPosition) {}, - buildSpaceContainer: (index) { + buildSpaceContainer: () { return Opacity( - opacity: 1.0, + opacity: widget.selectedSpace == null + ? 1.0 + : widget.selectedSpace?.uuid != space.uuid + ? 0.5 + : 1.0, child: SpaceCell( - index: index, - onTap: () => _onSpaceTapped(space.uuid), + onTap: () => _onSpaceTapped(space), icon: space.icon, name: space.spaceName, ), ); }, - screenSize: MediaQuery.sizeOf(context), - position: position, - isHovered: false, - onHoverChanged: (int index, bool isHovered) {}, - onButtonTap: (int index, Offset newPosition) {}, + onTap: () => showDialog( + context: context, + builder: (context) => const Text('123'), + ), ), ), ); @@ -218,7 +242,7 @@ class _CommunityStructureCanvasState extends State transformationController: _transformationController, boundaryMargin: EdgeInsets.symmetric( horizontal: MediaQuery.sizeOf(context).width * 0.3, - vertical: MediaQuery.sizeOf(context).height * 0.2, + vertical: MediaQuery.sizeOf(context).height * 0.3, ), minScale: 0.5, maxScale: 3.0, @@ -226,8 +250,8 @@ class _CommunityStructureCanvasState extends State child: GestureDetector( onTap: _resetSelectionAndZoom, child: SizedBox( - width: MediaQuery.sizeOf(context).width * 2, - height: MediaQuery.sizeOf(context).height * 2, + width: MediaQuery.sizeOf(context).width * 5, + height: MediaQuery.sizeOf(context).height * 5, child: Stack(children: treeWidgets), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart index 755a6ab9..68169861 100644 --- a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -2,15 +2,11 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class PlusButtonWidget extends StatelessWidget { - final int index; - final String direction; final Offset offset; - final void Function(int index, Offset newPosition) onButtonTap; + final void Function() onButtonTap; const PlusButtonWidget({ super.key, - required this.index, - required this.direction, required this.offset, required this.onButtonTap, }); @@ -18,13 +14,7 @@ class PlusButtonWidget extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - if (direction == 'down') { - onButtonTap(index, const Offset(0, 150)); - } else { - onButtonTap(index, const Offset(150, 0)); - } - }, + onTap: onButtonTap, child: Container( width: 30, height: 30, diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart index 1ce28502..e91e577f 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -1,60 +1,39 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; -class SpaceCardWidget extends StatelessWidget { - final int index; - final Size screenSize; - final Offset position; - final bool isHovered; - final void Function(int index, bool isHovered) onHoverChanged; - final void Function(int index, Offset newPosition) onButtonTap; - final Widget Function(int index) buildSpaceContainer; - final ValueChanged onPositionChanged; +class SpaceCardWidget extends StatefulWidget { + final void Function() onTap; + final Widget Function() buildSpaceContainer; const SpaceCardWidget({ - super.key, - required this.index, - required this.onPositionChanged, - required this.screenSize, - required this.position, - required this.isHovered, - required this.onHoverChanged, - required this.onButtonTap, + required this.onTap, required this.buildSpaceContainer, + super.key, }); + @override + State createState() => _SpaceCardWidgetState(); +} + +class _SpaceCardWidgetState extends State { + bool isHovered = false; @override Widget build(BuildContext context) { return MouseRegion( - onEnter: (_) => onHoverChanged(index, true), - onExit: (_) => onHoverChanged(index, false), + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), child: SizedBox( - width: 150, - height: 90, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ - buildSpaceContainer(index), - + widget.buildSpaceContainer(), if (isHovered) Positioned( bottom: 0, child: PlusButtonWidget( - index: index, - direction: 'down', offset: Offset.zero, - onButtonTap: onButtonTap, - ), - ), - if (isHovered) - Positioned( - right: -15, - child: PlusButtonWidget( - index: index, - direction: 'right', - offset: Offset.zero, - onButtonTap: onButtonTap, + onButtonTap: widget.onTap, ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart index 1b08835a..bcde6560 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -1,29 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceCell extends StatelessWidget { - final int index; final String icon; final String name; - final VoidCallback? onDoubleTap; final VoidCallback? onTap; const SpaceCell({ super.key, - required this.index, required this.icon, required this.name, - this.onTap, - this.onDoubleTap, + required this.onTap, }); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return GestureDetector( - onDoubleTap: onDoubleTap, onTap: onTap, child: Container( width: 150, @@ -36,7 +30,7 @@ class SpaceCell extends StatelessWidget { Expanded( child: Text( name, - style: theme.textTheme.bodyLarge?.copyWith( + style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, color: ColorsManager.blackColor, ), @@ -63,7 +57,10 @@ class SpaceCell extends StatelessWidget { child: Center( child: SvgPicture.asset( icon, - color: ColorsManager.whiteColors, + colorFilter: const ColorFilter.mode( + ColorsManager.whiteColors, + BlendMode.srcIn, + ), width: 24, height: 24, ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 11ee5078..6fe80835 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -9,14 +9,18 @@ class SpaceManagementCommunityStructure extends StatelessWidget { @override Widget build(BuildContext context) { - final selectedCommunity = - context.watch().state.selectedCommunity!; + final selectionBloc = context.watch().state; + final selectedCommunity = selectionBloc.selectedCommunity; + final selectedSpace = selectionBloc.selectedSpace; const spacer = Spacer(flex: 10); return Visibility( - visible: selectedCommunity.spaces.isNotEmpty, + visible: selectedCommunity!.spaces.isNotEmpty, replacement: const Row( children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), - child: CommunityStructureCanvas(community: selectedCommunity), + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), ); } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 40a41f74..21088632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -16,7 +16,7 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel space; + final SpaceModel? space; final CommunityModel community; const SelectSpaceEvent({required this.space, required this.community}); From 87b45fff1dfee05f8ae716ca7eed31589864d6db Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:21:25 +0300 Subject: [PATCH 053/105] removed expanded widget that caused a size exception. --- .../space_management_templates_view.dart | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart index dd46d2c1..138dbbc4 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -7,26 +7,24 @@ class SpaceManagementTemplatesView extends StatelessWidget { const SpaceManagementTemplatesView({super.key}); @override Widget build(BuildContext context) { - return Expanded( - child: ColoredBox( - color: ColorsManager.whiteColors, - child: GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 400, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 2.0, - ), - itemCount: _gridItems(context).length, - itemBuilder: (context, index) { - final model = _gridItems(context)[index]; - return CommunityTemplateCell( - onTap: model.onTap, - title: model.title, - ); - }, + return ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, ), ); } From 0fb91496135e059fc00d2aea2f13f62b253a4ec1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:26:56 +0300 Subject: [PATCH 054/105] Selects all children of a space when selecting a parent. --- .../spaces_connections_arrow_painter.dart | 10 +++--- .../widgets/community_structure_canvas.dart | 36 ++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index fcf523bf..cad82a60 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -7,21 +7,21 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final Map positions; final double cardWidth = 150.0; final double cardHeight = 90.0; - final String? selectedSpaceUuid; + final Set highlightedUuids; SpacesConnectionsArrowPainter({ required this.connections, required this.positions, - this.selectedSpaceUuid, + required this.highlightedUuids, }); @override void paint(Canvas canvas, Size size) { for (final connection in connections) { - final isSelected = connection.to == selectedSpaceUuid; + final isSelected = highlightedUuids.contains(connection.from); final paint = Paint() ..color = isSelected - ? ColorsManager.primaryColor + ? ColorsManager.blackColor : ColorsManager.blackColor.withValues(alpha: 0.5) ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; @@ -46,7 +46,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final circlePaint = Paint() ..color = isSelected - ? ColorsManager.primaryColor + ? ColorsManager.blackColor : ColorsManager.blackColor.withValues(alpha: 0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.srcIn; diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 22b4536a..04795c11 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -62,6 +62,15 @@ class _CommunityStructureCanvasState extends State super.dispose(); } + Set _getAllDescendantUuids(SpaceModel space) { + final uuids = {}; + for (final child in space.children) { + uuids.add(child.uuid); + uuids.addAll(_getAllDescendantUuids(child)); + } + return uuids; + } + void _runAnimation(Matrix4 target) { final animation = Matrix4Tween( begin: _transformationController.value, @@ -174,16 +183,23 @@ class _CommunityStructureCanvasState extends State _calculateLayout(community.spaces, 0, {}); + final selectedSpace = widget.selectedSpace; + final highlightedUuids = {}; + if (selectedSpace != null) { + highlightedUuids.add(selectedSpace.uuid); + highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace)); + } + final widgets = []; final connections = []; - _generateWidgets(community.spaces, widgets, connections); + _generateWidgets(community.spaces, widgets, connections, highlightedUuids); return [ CustomPaint( painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, - selectedSpaceUuid: widget.selectedSpace?.uuid, + highlightedUuids: highlightedUuids, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), ), @@ -194,11 +210,15 @@ class _CommunityStructureCanvasState extends State List spaces, List widgets, List connections, + Set highlightedUuids, ) { for (final space in spaces) { final position = _positions[space.uuid]; if (position == null) continue; + final isHighlighted = highlightedUuids.contains(space.uuid); + final hasNoSelectedSpace = widget.selectedSpace == null; + widgets.add( Positioned( left: position.dx, @@ -208,11 +228,7 @@ class _CommunityStructureCanvasState extends State child: SpaceCardWidget( buildSpaceContainer: () { return Opacity( - opacity: widget.selectedSpace == null - ? 1.0 - : widget.selectedSpace?.uuid != space.uuid - ? 0.5 - : 1.0, + opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, child: SpaceCell( onTap: () => _onSpaceTapped(space), icon: space.icon, @@ -229,9 +245,11 @@ class _CommunityStructureCanvasState extends State ); for (final child in space.children) { - connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + connections.add( + SpaceConnectionModel(from: space.uuid, to: child.uuid), + ); } - _generateWidgets(space.children, widgets, connections); + _generateWidgets(space.children, widgets, connections, highlightedUuids); } } From 329b2ba472ca1f0b4591ef4142911eb35c63f6df Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:36:13 +0300 Subject: [PATCH 055/105] selects the space from and to connection when selecting a space. --- .../painters/spaces_connections_arrow_painter.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index cad82a60..e9fa0a15 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -18,7 +18,8 @@ class SpacesConnectionsArrowPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { for (final connection in connections) { - final isSelected = highlightedUuids.contains(connection.from); + final isSelected = highlightedUuids.contains(connection.from) || + highlightedUuids.contains(connection.to); final paint = Paint() ..color = isSelected ? ColorsManager.blackColor @@ -36,7 +37,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final path = Path()..moveTo(startPoint.dx, startPoint.dy); - final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, From 90f8305aa1baaee3536000415ff5bf79d739c9a0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:45:13 +0300 Subject: [PATCH 056/105] shows tooltip on `SpaceCell`. --- .../widgets/community_structure_canvas.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 04795c11..5d38cfa2 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -229,10 +229,14 @@ class _CommunityStructureCanvasState extends State buildSpaceContainer: () { return Opacity( opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, - child: SpaceCell( - onTap: () => _onSpaceTapped(space), - icon: space.icon, - name: space.spaceName, + child: Tooltip( + message: space.spaceName, + preferBelow: false, + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, + ), ), ); }, From 5a2299ea2f166cd812409af66f0803be252f4d5f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:47:48 +0300 Subject: [PATCH 057/105] navigates to initial create space dialog from the respective buttons. --- .../widgets/community_structure_canvas.dart | 6 ++---- .../main_module/widgets/create_space_button.dart | 3 ++- .../space_management_community_structure.dart | 3 ++- .../helpers/space_details_dialog_helper.dart | 11 +++++++++++ .../presentation/widgets/space_details_dialog.dart | 12 ++++++++++++ 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 5d38cfa2..4aea103a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ @@ -240,10 +241,7 @@ class _CommunityStructureCanvasState extends State ), ); }, - onTap: () => showDialog( - context: context, - builder: (context) => const Text('123'), - ), + onTap: () => SpaceDetailsDialogHelper.showCreate(context), ), ), ); diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index 5caf6a81..4cbfd7fd 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CreateSpaceButton extends StatelessWidget { @@ -7,7 +8,7 @@ class CreateSpaceButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () {}, + onTap: () => SpaceDetailsDialogHelper.showCreate(context), child: Container( height: 60, decoration: BoxDecoration( diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 6fe80835..99d0668a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -16,7 +16,8 @@ class SpaceManagementCommunityStructure extends StatelessWidget { return Visibility( visible: selectedCommunity!.spaces.isNotEmpty, replacement: const Row( - children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + children: [spacer, Expanded(child: CreateSpaceButton()), spacer], + ), child: CommunityStructureCanvas( community: selectedCommunity, selectedSpace: selectedSpace, diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart new file mode 100644 index 00000000..e871f4d0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; + +abstract final class SpaceDetailsDialogHelper { + static void showCreate(BuildContext context) { + showDialog( + context: context, + builder: (context) => const SpaceDetailsDialog(), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart new file mode 100644 index 00000000..7213c99e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class SpaceDetailsDialog extends StatelessWidget { + const SpaceDetailsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const Dialog( + child: Text('Create Space'), + ); + } +} From 2a2fb7ffca48658623ec6f366ac9b97c61b9892f Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 11:36:50 +0300 Subject: [PATCH 058/105] Add responsive input fields and radio groups for visitor password setup --- .../view/access_type_radio_group.dart | 120 ++++ .../view/responsive_fields_row.dart | 73 +++ .../view/usage_frequency_radio_group.dart | 91 +++ .../view/visitor_password_dialog.dart | 589 +++++++++--------- 4 files changed, 574 insertions(+), 299 deletions(-) create mode 100644 lib/pages/visitor_password/view/access_type_radio_group.dart create mode 100644 lib/pages/visitor_password/view/responsive_fields_row.dart create mode 100644 lib/pages/visitor_password/view/usage_frequency_radio_group.dart diff --git a/lib/pages/visitor_password/view/access_type_radio_group.dart b/lib/pages/visitor_password/view/access_type_radio_group.dart new file mode 100644 index 00000000..be4adb9d --- /dev/null +++ b/lib/pages/visitor_password/view/access_type_radio_group.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; + +class AccessTypeRadioGroup extends StatelessWidget { + final String? selectedType; + final String? accessTypeSelected; + final Function(String) onTypeSelected; + final VisitorPasswordBloc visitorBloc; + + const AccessTypeRadioGroup({ + super.key, + required this.selectedType, + required this.accessTypeSelected, + required this.onTypeSelected, + required this.visitorBloc, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + Text('Access Type', style: text), + ], + ), + const SizedBox(height: 8), + if (size.width < 800) + Column( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + ], + ) + else + Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + ], + ), + ), + const Spacer(flex: 2), + ], + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + }) { + return SizedBox( + width: width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black, + fontSize: 13, + )), + value: value, + groupValue: groupValue, + onChanged: (value) { + if (value != null) { + onChanged(value); + if (value == 'Dynamic Password') { + visitorBloc.usageFrequencySelected = ''; + } + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/responsive_fields_row.dart b/lib/pages/visitor_password/view/responsive_fields_row.dart new file mode 100644 index 00000000..92a79276 --- /dev/null +++ b/lib/pages/visitor_password/view/responsive_fields_row.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; + +class NameAndEmailFields extends StatelessWidget { + final TextEditingController nameController; + final TextEditingController emailController; + final String? Function(String?)? nameValidator; + final String? Function(String?)? emailValidator; + + const NameAndEmailFields({ + super.key, + required this.nameController, + required this.emailController, + required this.nameValidator, + required this.emailValidator, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return Container( + width: size.width, + child: size.width < 800 + ? Column( + children: [ + CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + const SizedBox(height: 15), + CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ], + ) + : Row( + children: [ + Expanded( + flex: 2, + child: CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ), + const Spacer(), + ], + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/usage_frequency_radio_group.dart b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart new file mode 100644 index 00000000..aebebefe --- /dev/null +++ b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class UsageFrequencyRadioGroup extends StatelessWidget { + final String? selectedFrequency; + final String? usageFrequencySelected; + final Function(String) onFrequencySelected; + + const UsageFrequencyRadioGroup({ + super.key, + required this.selectedFrequency, + required this.usageFrequencySelected, + required this.onFrequencySelected, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return size.width < 600 + ? Column( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + ], + ) + : Row( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + required TextStyle text, + bool fullWidth = false, + }) { + return SizedBox( + width: fullWidth ? double.infinity : width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, style: text), + value: value, + groupValue: groupValue, + onChanged: (String? value) { + if (value != null) { + onChanged(value); + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 1e43af46..4b5cb0e2 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -9,8 +9,11 @@ import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/access_type_radio_group.dart'; import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart'; +import 'package:syncrow_web/pages/visitor_password/view/responsive_fields_row.dart'; +import 'package:syncrow_web/pages/visitor_password/view/usage_frequency_radio_group.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -21,7 +24,10 @@ class VisitorPasswordDialog extends StatelessWidget { @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; - var text = Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black, fontSize: 13); + var text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); return BlocProvider( create: (context) => VisitorPasswordBloc(), child: BlocListener( @@ -35,7 +41,8 @@ class VisitorPasswordDialog extends StatelessWidget { title: 'Sent Successfully', widgeta: Column( children: [ - if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.failedOperations.isNotEmpty) Column( children: [ const Text('Failed Devices'), @@ -45,7 +52,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.failedOperations.length, + itemCount: visitorBloc + .passwordStatus!.failedOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), @@ -53,14 +61,17 @@ class VisitorPasswordDialog extends StatelessWidget { height: 45, child: Center( child: Text(visitorBloc - .passwordStatus!.failedOperations[index].deviceUuid)), + .passwordStatus! + .failedOperations[index] + .deviceUuid)), ); }, ), ), ], ), - if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.successOperations.isNotEmpty) Column( children: [ const Text('Success Devices'), @@ -70,15 +81,18 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.successOperations.length, + itemCount: visitorBloc + .passwordStatus!.successOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc.passwordStatus! - .successOperations[index].deviceUuid)), + child: Text(visitorBloc + .passwordStatus! + .successOperations[index] + .deviceUuid)), ); }, ), @@ -89,7 +103,6 @@ class VisitorPasswordDialog extends StatelessWidget { )) .then((v) { Navigator.of(context).pop(true); - }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -102,15 +115,16 @@ class VisitorPasswordDialog extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, VisitorPasswordState state) { final visitorBloc = BlocProvider.of(context); - bool isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; + bool isRepeat = + state is IsRepeatState ? state.repeat : visitorBloc.repeat; return AlertDialog( backgroundColor: Colors.white, title: Text( 'Create visitor password', - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith(fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 24, + color: Colors.black), ), content: state is LoadingInitialState ? const Center(child: CircularProgressIndicator()) @@ -121,34 +135,11 @@ class VisitorPasswordDialog extends StatelessWidget { padding: const EdgeInsets.all(5.0), child: ListBody( children: [ - Container( - child: Row( - children: [ - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validate, - controller: visitorBloc.userNameController, - isRequired: true, - textFieldName: 'Name', - description: '', - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validateEmail, - controller: visitorBloc.emailController, - isRequired: true, - textFieldName: 'Email Address', - description: - 'The password will be sent to the visitor’s email address.', - ), - ), - const Spacer(), - ], - ), + NameAndEmailFields( + nameController: visitorBloc.userNameController, + emailController: visitorBloc.emailController, + nameValidator: visitorBloc.validate, + emailValidator: visitorBloc.validateEmail, ), const SizedBox( height: 15, @@ -156,107 +147,43 @@ class VisitorPasswordDialog extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text('Access Type', style: text), - ], + AccessTypeRadioGroup( + selectedType: state is PasswordTypeSelected + ? state.selectedType + : null, + accessTypeSelected: + visitorBloc.accessTypeSelected, + onTypeSelected: (value) { + context + .read() + .add(SelectPasswordType(value)); + }, + visitorBloc: visitorBloc, ), - Row( - children: [ - Expanded( - flex: 2, - child: Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Online Password', - style: text, - ), - value: 'Online Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Offline Password', style: text), - value: 'Offline Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - // SizedBox( - // width: size.width * 0.12, - // child: RadioListTile( - // contentPadding: EdgeInsets.zero, - // title: Text( - // 'Dynamic Password', - // style: text, - // ), - // value: 'Dynamic Password', - // groupValue: (state is PasswordTypeSelected) - // ? state.selectedType - // : visitorBloc.accessTypeSelected, - // onChanged: (String? value) { - // if (value != null) { - // context - // .read() - // .add(SelectPasswordType(value)); - // visitorBloc.usageFrequencySelected = ''; - // } - // }, - // ), - // ), - ], - )), - const Spacer( - flex: 2, - ), - ], - ), - if (visitorBloc.accessTypeSelected == 'Online Password') + + if (visitorBloc.accessTypeSelected == + 'Online Password') Text( 'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), - if (visitorBloc.accessTypeSelected == 'Offline Password') + if (visitorBloc.accessTypeSelected == + 'Offline Password') Text( 'Unaffected by the online status of the device, you can select online or offline device, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), // if (visitorBloc.accessTypeSelected == 'Dynamic Password') // Text( @@ -271,143 +198,170 @@ class VisitorPasswordDialog extends StatelessWidget { ) ], ), - visitorBloc.accessTypeSelected == 'Dynamic Password' - ? const SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (visitorBloc.accessTypeSelected == + 'Dynamic Password') + const SizedBox() + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text( - 'Usage Frequency', - style: text, - ), - ], + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), ), - Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'One-Time', - style: text, - ), - value: 'One-Time', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Periodic', style: text), - value: 'Periodic', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context.read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - ], + Text( + 'Usage Frequency', + style: text, ), - - //One-Time - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - // Periodic - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), ], ), + UsageFrequencyRadioGroup( + selectedFrequency: + state is UsageFrequencySelected + ? state.selectedFrequency + : null, + usageFrequencySelected: + visitorBloc.usageFrequencySelected, + onFrequencySelected: (value) { + context + .read() + .add(SelectUsageFrequency(value)); + }, + ), + + //One-Time + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, each device can be unlocked only once.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + // Periodic + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + ], + ), const SizedBox( height: 20, ), - if ((visitorBloc.usageFrequencySelected != 'One-Time' || - visitorBloc.accessTypeSelected != 'Offline Password') && + if ((visitorBloc.usageFrequencySelected != + 'One-Time' || + visitorBloc.accessTypeSelected != + 'Offline Password') && (visitorBloc.usageFrequencySelected != '')) DateTimeWebWidget( isRequired: true, title: 'Access Period', size: size, endTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add(SelectTimeEvent(context: context, isEffective: false)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: false)); } else { - visitorBloc.add(SelectTimeVisitorPassword(context: context, isStart: false, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); } }, startTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add( - SelectTimeEvent(context: context, isEffective: true)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: true)); } else { - visitorBloc.add(SelectTimeVisitorPassword( - context: context, isStart: true, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: true, + isRepeat: false)); } }, - firstString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + firstString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.effectiveTime - : visitorBloc.startTimeAccess.toString(), - secondString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + : visitorBloc.startTimeAccess + .toString(), + secondString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.expirationTime : visitorBloc.endTimeAccess.toString(), icon: Assets.calendarIcon), - const SizedBox(height: 10,), - Text(visitorBloc.accessPeriodValidate, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.red),), + const SizedBox( + height: 10, + ), + Text( + visitorBloc.accessPeriodValidate, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.red), + ), const SizedBox( height: 20, ), @@ -431,16 +385,21 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), const SizedBox( height: 20, ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') SizedBox( width: 100, child: Column( @@ -451,7 +410,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: CupertinoSwitch( value: visitorBloc.repeat, onChanged: (value) { - visitorBloc.add(ToggleRepeatEvent()); + visitorBloc + .add(ToggleRepeatEvent()); }, applyTheme: true, ), @@ -459,12 +419,16 @@ class VisitorPasswordDialog extends StatelessWidget { ], ), ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - isRepeat ? const RepeatWidget() : const SizedBox(), + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + isRepeat + ? const RepeatWidget() + : const SizedBox(), Container( decoration: containerDecoration, - width: size.width / 9, + width: size.width / 6, child: DefaultButton( onPressed: () { showDialog( @@ -472,22 +436,28 @@ class VisitorPasswordDialog extends StatelessWidget { barrierDismissible: false, builder: (BuildContext context) { return AddDeviceDialog( - selectedDeviceIds: visitorBloc.selectedDevices, + selectedDeviceIds: + visitorBloc.selectedDevices, ); }, ).then((listDevice) { if (listDevice != null) { - visitorBloc.selectedDevices = listDevice; + visitorBloc.selectedDevices = + listDevice; } }); }, borderRadius: 8, child: Text( '+ Add Device', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.whiteColors, - fontSize: 12), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: + ColorsManager.whiteColors, + fontSize: 12), ), ), ), @@ -525,30 +495,37 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { if (visitorBloc.forgetFormKey.currentState!.validate()) { if (visitorBloc.selectedDevices.isNotEmpty) { - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { setPasswordFunction(context, size, visitorBloc); - } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { if (visitorBloc.expirationTime != 'End Time' && - visitorBloc.effectiveTime != 'Start Time' ) { + visitorBloc.effectiveTime != 'Start Time') { setPasswordFunction(context, size, visitorBloc); - }else{ + } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - } else if( - visitorBloc.endTimeAccess.toString()!='End Time' - &&visitorBloc.startTimeAccess.toString()!='Start Time') { + } else if (visitorBloc.endTimeAccess.toString() != + 'End Time' && + visitorBloc.startTimeAccess.toString() != + 'Start Time') { if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { if (isRepeat == true) { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time' && visitorBloc.selectedDays.isNotEmpty) { - setPasswordFunction(context, size, visitorBloc); + setPasswordFunction( + context, size, visitorBloc); } else { visitorBloc.stateDialog( context: context, @@ -562,14 +539,16 @@ class VisitorPasswordDialog extends StatelessWidget { } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - }else{ - visitorBloc.stateDialog( - context: context, - message: 'Please select Access Period to continue', - title: 'Access Period'); + } else { + visitorBloc.stateDialog( + context: context, + message: + 'Please select Access Period to continue', + title: 'Access Period'); } } else { visitorBloc.stateDialog( @@ -615,7 +594,8 @@ class VisitorPasswordDialog extends StatelessWidget { content: SizedBox( height: size.height * 0.25, child: Center( - child: CircularProgressIndicator(), // Display a loading spinner + child: + CircularProgressIndicator(), // Display a loading spinner ), ), ); @@ -639,7 +619,10 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Set Password', - style: Theme.of(context).textTheme.headlineLarge!.copyWith( + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith( fontSize: 30, fontWeight: FontWeight.w400, color: Colors.black, @@ -689,37 +672,45 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { Navigator.pop(context); if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); - } - else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); } }, From 7e5825de45aa610348c91e45e94b11aac7284758 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:44:45 +0300 Subject: [PATCH 059/105] Fixed typo in occupancy sidebar. --- .../modules/occupancy/widgets/occupancy_end_side_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 3dd01bee..555841ca 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -23,7 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AnalyticsSidebarHeader(title: 'Presnce Sensor'), + const AnalyticsSidebarHeader(title: 'Presence Sensor'), Expanded( child: SizedBox( // height: MediaQuery.sizeOf(context).height * 0.2, From 6e6ef79ed0c8f9c99cbdb3be66577e50305c7f4d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:44:56 +0300 Subject: [PATCH 060/105] enhanced heat map tooltip's message. --- .../analytics/modules/occupancy/widgets/heat_map_tooltip.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart index c7695064..66612a3e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart @@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget { ), const Divider(height: 2, thickness: 1), Text( - '$value Occupants', + 'Occupancy detected: $value', style: context.textTheme.bodySmall?.copyWith( fontSize: 10, fontWeight: FontWeight.w500, From ee1ebeae2e904f06221bbc9411d9094a78f3718e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:45:15 +0300 Subject: [PATCH 061/105] Changed energy management charts titles for a more clear name. --- .../widgets/energy_consumption_per_device_chart_box.dart | 2 +- .../widgets/total_energy_consumption_chart_box.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index be5faf57..06b6c529 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( - title: Text('Energy Consumption per Device'), + title: Text('Device energy consumed'), ), ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index e197c297..4d88471d 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { child: FittedBox( alignment: AlignmentDirectional.centerStart, fit: BoxFit.scaleDown, - child: ChartTitle(title: Text('Total Energy Consumption')), + child: ChartTitle(title: Text('Space energy consumed')), ), ), const Spacer(flex: 4), From 010403f1fa83ad82432a6933c29631258f5d5ce8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:50:22 +0300 Subject: [PATCH 062/105] Added day of month axis name to all charts. --- .../widgets/aqi_distribution_chart.dart | 2 ++ .../energy_management_charts_helper.dart | 11 ++++----- .../occupancy/widgets/occupancy_chart.dart | 8 ++++--- .../widgets/charts_x_axis_title.dart | 23 +++++++++++++++++++ 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 lib/pages/analytics/widgets/charts_x_axis_title.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 2f3d7ff0..63e1d0d9 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -139,6 +140,7 @@ class AqiDistributionChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) => FittedBox( diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index b1af85c8..6b44e125 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper { return FlTitlesData( show: true, bottomTitles: AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), drawBelowEverything: true, sideTitles: SideTitles( interval: 1, @@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper { ); } - static String getToolTipLabel(num month, double value) { - final monthLabel = month.toString(); - final valueLabel = value.formatNumberToKwh; - final labels = [monthLabel, valueLabel]; - return labels.where((element) => element.isNotEmpty).join(', '); - } + static String getToolTipLabel(double value) => value.formatNumberToKwh; static List getTooltipItems(List touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( - getToolTipLabel(spot.x, spot.y), + getToolTipLabel(spot.y), const TextStyle( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w600, diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 70087c46..1205a66e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget { }) { final data = chartData; - final occupancyValue = double.parse(data[group.x.toInt()].occupancy); - final percentage = '${(occupancyValue).toStringAsFixed(0)}%'; + final occupancyValue = double.parse(data[group.x].occupancy); + final percentage = '${occupancyValue.toStringAsFixed(0)}%'; return BarTooltipItem( percentage, @@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget { alignment: AlignmentDirectional.centerStart, fit: BoxFit.scaleDown, child: Text( - '${(value).toStringAsFixed(0)}%', + '${value.toStringAsFixed(0)}%', style: context.textTheme.bodySmall?.copyWith( fontSize: 12, color: ColorsManager.greyColor, @@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) => FittedBox( diff --git a/lib/pages/analytics/widgets/charts_x_axis_title.dart b/lib/pages/analytics/widgets/charts_x_axis_title.dart new file mode 100644 index 00000000..746a8cbb --- /dev/null +++ b/lib/pages/analytics/widgets/charts_x_axis_title.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ChartsXAxisTitle extends StatelessWidget { + const ChartsXAxisTitle({ + this.label = 'Day of month', + super.key, + }); + + final String label; + + @override + Widget build(BuildContext context) { + return Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ); + } +} From f901983aa57296c8339ee3a6a9eccb17c10c2eed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 15:01:25 +0300 Subject: [PATCH 063/105] Implemented ranges for the values in the AQI chart based on the selected pollutant. --- .../helpers/range_of_aqi_charts_helper.dart | 11 ++++++-- .../widgets/range_of_aqi_chart.dart | 28 +++++++++++++++++-- .../widgets/range_of_aqi_chart_box.dart | 7 ++++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 21cb2a9e..17b00506 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -18,7 +18,11 @@ abstract final class RangeOfAqiChartsHelper { (ColorsManager.hazardousPurple, 'Hazardous'), ]; - static FlTitlesData titlesData(BuildContext context, List data) { + static FlTitlesData titlesData( + BuildContext context, + List data, { + double leftSideInterval = 50, + }) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( @@ -38,10 +42,11 @@ abstract final class RangeOfAqiChartsHelper { leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 50, + interval: leftSideInterval, maxIncluded: false, + minIncluded: true, getTitlesWidget: (value, meta) { - final text = value >= 300 ? '301+' : value.toInt().toString(); + final text = value.toInt().toString(); return Padding( padding: const EdgeInsetsDirectional.only(end: 12), child: FittedBox( diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart index 5e731d90..0914eab3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RangeOfAqiChart extends StatelessWidget { final List chartData; + final AqiType selectedAqiType; const RangeOfAqiChart({ super.key, required this.chartData, + required this.selectedAqiType, }); List<(List values, Color color, Color? dotColor)> get _lines { @@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget { ]; } + (double maxY, double interval) get _maxYForAqiType { + const aqiMaxValues = { + AqiType.aqi: (401, 100), + AqiType.pm25: (351, 50), + AqiType.pm10: (501, 100), + AqiType.hcho: (301, 50), + AqiType.tvoc: (501, 50), + AqiType.co2: (1251, 250), + }; + + return aqiMaxValues[selectedAqiType]!; + } + @override Widget build(BuildContext context) { return LineChart( LineChartData( minY: 0, - maxY: 301, + maxY: _maxYForAqiType.$1, clipData: const FlClipData.vertical(), - gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: _maxYForAqiType.$2, + ), + titlesData: RangeOfAqiChartsHelper.titlesData( + context, + chartData, + leftSideInterval: _maxYForAqiType.$2, + ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), betweenBarsData: [ diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index 6548c696..cb189dce 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), + Expanded( + child: RangeOfAqiChart( + chartData: state.filteredRangeOfAqi, + selectedAqiType: state.selectedAqiType, + ), + ), ], ), ); From 277a9ce4f007e472571c10fcbb909f837620f892 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 15:38:16 +0300 Subject: [PATCH 064/105] Add countdown seconds to schedule management --- .../schedule_device/bloc/schedule_bloc.dart | 14 +++- .../schedule_device/bloc/schedule_event.dart | 4 +- .../schedule_device/bloc/schedule_state.dart | 13 +++- .../count_down_inching_view.dart | 66 +++++++++++++++++-- .../schedule_widgets/schedual_view.dart | 4 +- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 62213205..fbf7ae64 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc { emit(currentState.copyWith( scheduleMode: event.scheduleMode, countdownRemaining: Duration.zero, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, )); } } @@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; emit(currentState.copyWith( + countdownSeconds: event.seconds, countdownHours: event.hours, countdownMinutes: event.minutes, inchingHours: 0, @@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc { inchingHours: event.hours, inchingMinutes: event.minutes, countdownRemaining: Duration.zero, + inchingSeconds: 0, // Add this )); } } @@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: countdownDuration.inMinutes % 60, countdownRemaining: countdownDuration, isCountdownActive: true, + countdownSeconds: countdownDuration.inSeconds, ), ); @@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: 0, countdownRemaining: Duration.zero, isCountdownActive: false, + countdownSeconds: 0, ), ); } @@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc { inchingMinutes: inchingDuration.inMinutes % 60, isInchingActive: true, countdownRemaining: inchingDuration, + countdownSeconds: inchingDuration.inSeconds, ), ); } @@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc { } String extractTime(String isoDateTime) { - // Example input: "2025-06-19T15:45:00.000" - return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00" + return isoDateTime.split('T')[1].split('.')[0]; } int? getTimeStampWithoutSeconds(DateTime? dateTime) { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 7ec144fe..0b9ec581 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -146,14 +146,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent { class UpdateCountdownTimeEvent extends ScheduleEvent { final int hours; final int minutes; + final int seconds; const UpdateCountdownTimeEvent({ required this.hours, required this.minutes, + required this.seconds, }); @override - List get props => [hours, minutes]; + List get props => [hours, minutes, seconds]; } class UpdateInchingTimeEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart index 10cd7611..63551c3a 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState { final bool isCountdownActive; final int inchingHours; final int inchingMinutes; + final int inchingSeconds; final bool isInchingActive; final ScheduleModes scheduleMode; final Duration? countdownRemaining; + final int? countdownSeconds; const ScheduleLoaded({ + this.countdownSeconds = 0, + this.inchingSeconds = 0, required this.schedules, this.selectedTime, required this.selectedDays, @@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState { bool? isInchingActive, ScheduleModes? scheduleMode, Duration? countdownRemaining, + String? deviceId, + int? countdownSeconds, + int? inchingSeconds, }) { return ScheduleLoaded( schedules: schedules ?? this.schedules, @@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState { selectedDays: selectedDays ?? this.selectedDays, functionOn: functionOn ?? this.functionOn, isEditing: isEditing ?? this.isEditing, - deviceId: deviceId, + deviceId: deviceId ?? this.deviceId, countdownHours: countdownHours ?? this.countdownHours, countdownMinutes: countdownMinutes ?? this.countdownMinutes, isCountdownActive: isCountdownActive ?? this.isCountdownActive, @@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive: isInchingActive ?? this.isInchingActive, scheduleMode: scheduleMode ?? this.scheduleMode, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + countdownSeconds: countdownSeconds ?? this.countdownSeconds, + inchingSeconds: inchingSeconds ?? this.inchingSeconds, ); } @@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive, scheduleMode, countdownRemaining, + countdownSeconds, + inchingSeconds, ]; } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart index d45073ec..418bab6c 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CountdownInchingView extends StatefulWidget { - const CountdownInchingView({super.key}); + final String deviceId; + const CountdownInchingView({super.key, required this.deviceId}); @override State createState() => _CountdownInchingViewState(); @@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget { class _CountdownInchingViewState extends State { late FixedExtentScrollController _hoursController; late FixedExtentScrollController _minutesController; + late FixedExtentScrollController _secondsController; int _lastHours = -1; int _lastMinutes = -1; + int _lastSeconds = -1; @override void initState() { super.initState(); _hoursController = FixedExtentScrollController(); _minutesController = FixedExtentScrollController(); + _secondsController = FixedExtentScrollController(); } @override void dispose() { _hoursController.dispose(); _minutesController.dispose(); + _secondsController.dispose(); super.dispose(); } - void _updateControllers(int displayHours, int displayMinutes) { + void _updateControllers( + int displayHours, int displayMinutes, int displaySeconds) { if (_lastHours != displayHours) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_hoursController.hasClients) { @@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State { }); _lastMinutes = displayMinutes; } + // Update seconds controller + if (_lastSeconds != displaySeconds) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_secondsController.hasClients) { + _secondsController.jumpToItem(displaySeconds); + } + }); + _lastSeconds = displaySeconds; + } } @override @@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State { return BlocBuilder( builder: (context, state) { if (state is! ScheduleLoaded) return const SizedBox.shrink(); - final isCountDown = state.scheduleMode == ScheduleModes.countdown; final isActive = isCountDown ? state.isCountdownActive : state.isInchingActive; @@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State { final displayMinutes = isActive && state.countdownRemaining != null ? state.countdownRemaining!.inMinutes.remainder(60) : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + final displaySeconds = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inSeconds.remainder(60) + : (isCountDown ? state.countdownSeconds : state.inchingSeconds); + + _updateControllers(displayHours, displayMinutes, displaySeconds!); + + if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) { + context.read().add( + StopScheduleEvent( + mode: ScheduleModes.countdown, + deviceId: widget.deviceId, + ), + ); + } - _updateControllers(displayHours, displayMinutes); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: value, minutes: displayMinutes)); + hours: value, + minutes: displayMinutes, + seconds: displaySeconds, + )); } }, isActive: isActive, @@ -115,11 +145,35 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: displayHours, minutes: value)); + hours: displayHours, + minutes: value, + seconds: displaySeconds, + )); } }, isActive: isActive, ), + const SizedBox(width: 10), + if (isActive) + _buildPickerColumn( + context, + 's', + displaySeconds, + 60, + _secondsController, + (value) { + if (!isActive) { + context + .read() + .add(UpdateCountdownTimeEvent( + hours: displayHours, + minutes: displayMinutes, + seconds: value, + )); + } + }, + isActive: isActive, + ), ], ), ], diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 2fa34559..47534d37 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -74,7 +74,9 @@ class BuildScheduleView extends StatelessWidget { ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - const CountdownInchingView(), + CountdownInchingView( + deviceId: deviceUuid, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.countdown) CountdownModeButtons( From c6e98fa24550d5452494e7c23a4d7a06d7201ccd Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 16:06:53 +0300 Subject: [PATCH 065/105] Refactor visitor password dialog navigation to return specific values on pop --- .../visitor_password/view/visitor_password_dialog.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4b5cb0e2..978b425a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -102,7 +102,7 @@ class VisitorPasswordDialog extends StatelessWidget { ], )) .then((v) { - Navigator.of(context).pop(true); + Navigator.of(context).pop(v); }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -476,7 +476,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(true); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( @@ -651,7 +651,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( From c649044a1fc3c98422cf9ba1a8ef15bdf4e3c19e Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 16:40:42 +0300 Subject: [PATCH 066/105] Enhance navigation buttons in SmartPowerDeviceControl for better user experience --- .../view/smart_power_device_control.dart | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart index 67313802..11d1cc8f 100644 --- a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -12,7 +12,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; //Smart Power Clamp -class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout { +class SmartPowerDeviceControl extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const SmartPowerDeviceControl({super.key, required this.deviceId}); @@ -145,13 +146,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou children: [ IconButton( icon: const Icon(Icons.arrow_left), - onPressed: () { - blocProvider.add(SmartPowerArrowPressedEvent(-1)); - pageController.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + onPressed: blocProvider.currentPage <= 0 + ? null + : () { + blocProvider + .add(SmartPowerArrowPressedEvent(-1)); + pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, ), Text( currentPage == 0 @@ -165,13 +169,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou ), IconButton( icon: const Icon(Icons.arrow_right), - onPressed: () { - blocProvider.add(SmartPowerArrowPressedEvent(1)); - pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + onPressed: blocProvider.currentPage >= 3 + ? null + : () { + blocProvider + .add(SmartPowerArrowPressedEvent(1)); + pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, ), ], ), @@ -195,8 +202,8 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou blocProvider.add(SelectDateEvent(context: context)); blocProvider.add(FilterRecordsByDateEvent( selectedDate: blocProvider.dateTime!, - viewType: - blocProvider.views[blocProvider.currentIndex])); + viewType: blocProvider + .views[blocProvider.currentIndex])); }, widget: blocProvider.dateSwitcher(), chartData: blocProvider.energyDataList.isNotEmpty From 52b843d51429935fbd2cd4b5290b34804fe06575 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 09:53:09 +0300 Subject: [PATCH 067/105] SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when-selecting-space-from-sidebar. --- lib/pages/space_tree/bloc/space_tree_bloc.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index e8c2e015..7d1a4d96 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -289,7 +289,6 @@ class SpaceTreeBloc extends Bloc { selectedSpaces: updatedSelectedSpaces, soldCheck: updatedSoldChecks, selectedCommunityAndSpaces: communityAndSpaces)); - emit(state.copyWith(selectedSpaces: updatedSelectedSpaces)); } catch (e) { emit(const SpaceTreeErrorState('Something went wrong')); } @@ -445,10 +444,12 @@ class SpaceTreeBloc extends Bloc { List _getThePathToChild(String communityId, String selectedSpaceId) { List ids = []; - for (var community in state.communityList) { + final communityDataSource = + state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; + for (final community in communityDataSource) { if (community.uuid == communityId) { - for (var space in community.spaces) { - List list = []; + for (final space in community.spaces) { + final list = []; list.add(space.uuid!); ids = _getAllParentsIds(space, selectedSpaceId, List.from(list)); if (ids.isNotEmpty) { From 562c67a958dbb3526ba677e4fc8f129a0b01af69 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 12:24:09 +0300 Subject: [PATCH 068/105] Add deviceName field to FailedOperation and SuccessOperation models --- .../visitor_password/model/failed_operation.dart | 13 ++++++++----- .../view/visitor_password_dialog.dart | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pages/visitor_password/model/failed_operation.dart b/lib/pages/visitor_password/model/failed_operation.dart index 223f9ac5..120f6d89 100644 --- a/lib/pages/visitor_password/model/failed_operation.dart +++ b/lib/pages/visitor_password/model/failed_operation.dart @@ -2,11 +2,13 @@ class FailedOperation { final bool success; final dynamic deviceUuid; final dynamic error; + final String deviceName; FailedOperation({ required this.success, required this.deviceUuid, required this.error, + required this.deviceName, }); factory FailedOperation.fromJson(Map json) { @@ -14,6 +16,7 @@ class FailedOperation { success: json['success'], deviceUuid: json['deviceUuid'], error: json['error'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -22,21 +25,22 @@ class FailedOperation { 'success': success, 'deviceUuid': deviceUuid, 'error': error, + 'deviceName': deviceName, }; } } - - class SuccessOperation { final bool success; // final Result result; final String deviceUuid; + final String deviceName; SuccessOperation({ required this.success, // required this.result, required this.deviceUuid, + required this.deviceName, }); factory SuccessOperation.fromJson(Map json) { @@ -44,6 +48,7 @@ class SuccessOperation { success: json['success'], // result: Result.fromJson(json['result']), deviceUuid: json['deviceUuid'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -52,6 +57,7 @@ class SuccessOperation { 'success': success, // 'result': result.toJson(), 'deviceUuid': deviceUuid, + 'deviceName': deviceName, }; } } @@ -92,8 +98,6 @@ class SuccessOperation { // } // } - - class PasswordStatus { final List successOperations; final List failedOperations; @@ -121,4 +125,3 @@ class PasswordStatus { }; } } - diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 978b425a..d1fb172a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: Text(visitorBloc .passwordStatus! .failedOperations[index] - .deviceUuid)), + .deviceName)), ); }, ), @@ -92,7 +92,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: Text(visitorBloc .passwordStatus! .successOperations[index] - .deviceUuid)), + .deviceName)), ); }, ), From 7397486e7ac3c948cc5e3de1e40b9267c3e25509 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:11:49 +0300 Subject: [PATCH 069/105] SP-368-Clarification-on-Default-Value-for-Start-Date-in-Door-Lock-Online-Tile-Limited-Password-repeat-section --- .../bloc/visitor_password_bloc.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 438b1abf..6dc20cfd 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -68,7 +68,7 @@ class VisitorPasswordBloc DateTime? startTime = DateTime.now(); DateTime? endTime; - String startTimeAccess = 'Start Time'; + String startTimeAccess = DateTime.now().toString().split('.').first; String endTimeAccess = 'End Time'; PasswordStatus? passwordStatus; selectAccessType( @@ -136,6 +136,27 @@ class VisitorPasswordBloc ); return; } + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text('Effective Time cannot be earlier than current time.'), + actionsAlignment: MainAxisAlignment.center, + content: + FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false)); + }, + child: const Text('OK'), + ), + + ), + ); + } + return; + } effectiveTimeTimeStamp = selectedTimestamp; startTimeAccess = selectedDateTime.toString().split('.').first; } else { From 5f5958369647da17bb4e346a56cae85bdcdd18f8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:12 +0300 Subject: [PATCH 070/105] Supported `NCPS` device type in occupancy devices dropdown. --- .../modules/occupancy/helpers/fetch_occupancy_data_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart index 0b01fda2..3bd96bce 100644 --- a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart +++ b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart @@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper { param: GetAnalyticsDevicesParam( communityUuid: communityUuid, spaceUuid: spaceUuid, - deviceTypes: ['WPS', 'CPS'], + deviceTypes: ['WPS', 'CPS', 'NCPS'], requestType: AnalyticsDeviceRequestType.occupancy, ), onSuccess: (device) { From 3b4952db0ae650b0af5aac028bc0ffb26ec3d238 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:30 +0300 Subject: [PATCH 071/105] fixed thrown exceptions in`AnalyticsDevice`. --- lib/pages/analytics/models/analytics_device.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 3340a41d..869de23f 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -25,8 +25,8 @@ class AnalyticsDevice { factory AnalyticsDevice.fromJson(Map json) { return AnalyticsDevice( - uuid: json['uuid'] as String, - name: json['name'] as String, + uuid: json['uuid'] as String? ?? '', + name: json['name'] as String? ?? '', createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, @@ -39,8 +39,8 @@ class AnalyticsDevice { ? ProductDevice.fromJson(json['productDevice'] as Map) : null, spaceUuid: json['spaceUuid'] as String?, - latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null, - longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null, + latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null, + longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null, ); } } From 6d667af7dcf099b48709cbbc7618cc067358e7ef Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:46 +0300 Subject: [PATCH 072/105] increased size of `OccupancyEndSideBar` in medium sized screens. --- .../modules/occupancy/views/analytics_occupancy_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart index 3a025254..56f8ce08 100644 --- a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart +++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart @@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget { child: Column( spacing: 32, children: [ - SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()), + SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()), SizedBox(height: height * 0.5, child: const OccupancyChartBox()), SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()), ], From 22070ca04a385817093b90880f8ddea48fba1539 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:26:07 +0300 Subject: [PATCH 073/105] removed unused comment. --- .../modules/occupancy/widgets/occupancy_end_side_bar.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 3dd01bee..75455a9b 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -26,7 +26,6 @@ class OccupancyEndSideBar extends StatelessWidget { const AnalyticsSidebarHeader(title: 'Presnce Sensor'), Expanded( child: SizedBox( - // height: MediaQuery.sizeOf(context).height * 0.2, child: PowerClampEnergyStatusWidget( status: [ PowerClampEnergyStatus( From 5e0df09cb6be5c8c8807a40a5cede8518cc2135f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:25:53 +0300 Subject: [PATCH 074/105] Changed tvoc unit to match the device. --- .../modules/air_quality/widgets/aqi_type_dropdown.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 5d482d9c..457bf610 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -7,7 +7,7 @@ enum AqiType { pm25('PM2.5', 'µg/m³', 'pm25'), pm10('PM10', 'µg/m³', 'pm10'), hcho('HCHO', 'mg/m³', 'cho2'), - tvoc('TVOC', 'µg/m³', 'voc'), + tvoc('TVOC', 'mg/m³', 'voc'), co2('CO2', 'ppm', 'co2'); const AqiType(this.value, this.unit, this.code); From e1bb67d7bd8074ffe2d0adb6e21d72cd0c7dfe4e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:26:05 +0300 Subject: [PATCH 075/105] reads correct value for TVOC. --- .../analytics/modules/air_quality/widgets/aqi_device_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index ebe88614..23ae874e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget { ); final tvocValue = _getValueForStatus( status, - 'tvoc_value', + 'voc_value', formatter: (value) => (value / 100).toStringAsFixed(2), ); From 520b73717ad886d3179179cedd3aa91d83c355bc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:34:07 +0300 Subject: [PATCH 076/105] Doesnt load devices when date changes. --- .../air_quality/helpers/fetch_air_quality_data_helper.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 223c0357..d7fbc279 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -24,11 +24,13 @@ abstract final class FetchAirQualityDataHelper { }) { final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; + if (shouldFetchAnalyticsDevices) { loadAnalyticsDevices( context, communityUuid: communityUuid, spaceUuid: spaceUuid, ); + } loadRangeOfAqi( context, spaceUuid: spaceUuid, From 30e940fdfc2fda32205c76ea0f4735ab28ea829f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:34:23 +0300 Subject: [PATCH 077/105] Reads the correct date to load aqi data. --- .../helpers/fetch_air_quality_data_helper.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index d7fbc279..5c63e397 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_qualit import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; -import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; @@ -22,14 +21,13 @@ abstract final class FetchAirQualityDataHelper { required String spaceUuid, bool shouldFetchAnalyticsDevices = true, }) { - final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; if (shouldFetchAnalyticsDevices) { - loadAnalyticsDevices( - context, - communityUuid: communityUuid, - spaceUuid: spaceUuid, - ); + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); } loadRangeOfAqi( context, From f38ac58442deb9172cdc702ee2dde1983749a0cb Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 14:45:10 +0300 Subject: [PATCH 078/105] Add bloc closure handling and improve device status updates in AcBloc --- .../device_managment/ac/bloc/ac_bloc.dart | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index af5a7b0a..eaababe1 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -16,6 +16,7 @@ class AcBloc extends Bloc { final ControlDeviceService controlDeviceService; final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; + bool _isBlocClosed = false; AcBloc({ required this.deviceId, @@ -45,7 +46,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -68,12 +70,13 @@ class AcBloc extends Bloc { } } - void _listenToChanges(deviceId) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; - - stream.listen((DatabaseEvent event) async { + _deviceStatusSubscription = + ref.onValue.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; Map usersMap = @@ -82,11 +85,15 @@ class AcBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); + statusList + .add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { + deviceStatus = + AcStatusModel.fromJson(usersMap['productUuid'], statusList); + print('Device status updated: ${deviceStatus.acSwitch}'); + + if (!_isBlocClosed) { add(AcStatusUpdated(deviceStatus)); } }); @@ -106,15 +113,14 @@ class AcBloc extends Bloc { Emitter emit, ) async { emit(AcsLoadingState()); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); try { final success = await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); if (!success) { emit(const AcsFailedState(error: 'Failed to control device')); } @@ -129,8 +135,10 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -293,13 +301,17 @@ class AcBloc extends Bloc { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - add(UpdateTimerEvent()); + if (!_isBlocClosed) { + add(UpdateTimerEvent()); + } } else { _countdownTimer?.cancel(); timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - add(TimerCompletedEvent()); + if (!_isBlocClosed) { + add(TimerCompletedEvent()); + } } }); } @@ -326,7 +338,9 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - add(UpdateTimerEvent()); + if (!_isBlocClosed) { + add(UpdateTimerEvent()); + } } } @@ -370,6 +384,9 @@ class AcBloc extends Bloc { @override Future close() { add(OnClose()); + _countdownTimer?.cancel(); + _deviceStatusSubscription?.cancel(); + _isBlocClosed = true; return super.close(); } } From 3c9494963d60a12767f559d84629fc6eee356079 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 15:58:58 +0300 Subject: [PATCH 079/105] Add generated configuration files for Flutter integration across platforms --- lib/pages/device_managment/ac/bloc/ac_bloc.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index eaababe1..38d11a46 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -16,7 +16,6 @@ class AcBloc extends Bloc { final ControlDeviceService controlDeviceService; final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; - bool _isBlocClosed = false; AcBloc({ required this.deviceId, @@ -93,7 +92,7 @@ class AcBloc extends Bloc { AcStatusModel.fromJson(usersMap['productUuid'], statusList); print('Device status updated: ${deviceStatus.acSwitch}'); - if (!_isBlocClosed) { + if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } }); @@ -301,7 +300,7 @@ class AcBloc extends Bloc { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - if (!_isBlocClosed) { + if (!isClosed) { add(UpdateTimerEvent()); } } else { @@ -309,7 +308,7 @@ class AcBloc extends Bloc { timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - if (!_isBlocClosed) { + if (!isClosed) { add(TimerCompletedEvent()); } } @@ -338,7 +337,7 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - if (!_isBlocClosed) { + if (!isClosed) { add(UpdateTimerEvent()); } } @@ -386,7 +385,6 @@ class AcBloc extends Bloc { add(OnClose()); _countdownTimer?.cancel(); _deviceStatusSubscription?.cancel(); - _isBlocClosed = true; return super.close(); } } From 6c268754a928c086f9e649de8824ed5194266cab Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:26:57 +0300 Subject: [PATCH 080/105] add icons and the basic route to show curtain module --- assets/icons/close_curtain.svg | 8 ++ assets/icons/open_curtain.svg | 8 ++ assets/icons/pause_curtain.svg | 6 + assets/icons/reverse_arrows.svg | 10 ++ .../helper/route_controls_based_code.dart | 113 ++++++++++++++---- 5 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 assets/icons/close_curtain.svg create mode 100644 assets/icons/open_curtain.svg create mode 100644 assets/icons/pause_curtain.svg create mode 100644 assets/icons/reverse_arrows.svg diff --git a/assets/icons/close_curtain.svg b/assets/icons/close_curtain.svg new file mode 100644 index 00000000..53f9e03b --- /dev/null +++ b/assets/icons/close_curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/open_curtain.svg b/assets/icons/open_curtain.svg new file mode 100644 index 00000000..715773a5 --- /dev/null +++ b/assets/icons/open_curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/pause_curtain.svg b/assets/icons/pause_curtain.svg new file mode 100644 index 00000000..8f90ea4f --- /dev/null +++ b/assets/icons/pause_curtain.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/reverse_arrows.svg b/assets/icons/reverse_arrows.svg new file mode 100644 index 00000000..fe119c39 --- /dev/null +++ b/assets/icons/reverse_arrows.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index 5586a310..cc09260e 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_s import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_batch.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_items.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart'; @@ -18,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dar import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart'; import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart'; @@ -39,8 +42,6 @@ import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heate import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart'; -import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; - mixin RouteControlsBasedCode { Widget routeControlsWidgets({required AllDevicesModel device}) { switch (device.productType) { @@ -84,6 +85,10 @@ mixin RouteControlsBasedCode { return CurtainStatusControlsView( deviceId: device.uuid!, ); + case 'CUR_2': + return CurtainModuleItems( + deviceId: device.uuid!, + ); case 'AC': return AcDeviceControlsView(device: device); case 'WH': @@ -107,7 +112,7 @@ mixin RouteControlsBasedCode { case 'SOS': return SosDeviceControlsView(device: device); - case 'NCPS': + case 'NCPS': return FlushMountedPresenceSensorControlView(device: device); default: return const SizedBox(); @@ -132,76 +137,140 @@ mixin RouteControlsBasedCode { switch (devices.first.productType) { case '1G': return WallLightBatchControlView( - deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '1G') + .map((e) => e.uuid!) + .toList(), ); case '2G': return TwoGangBatchControlView( - deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '2G') + .map((e) => e.uuid!) + .toList(), ); case '3G': return LivingRoomBatchControlsView( - deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '3G') + .map((e) => e.uuid!) + .toList(), ); case '1GT': return OneGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '1GT') + .map((e) => e.uuid!) + .toList(), ); case '2GT': return TwoGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '2GT') + .map((e) => e.uuid!) + .toList(), ); case '3GT': return ThreeGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '3GT') + .map((e) => e.uuid!) + .toList(), ); case 'GW': return GatewayBatchControlView( - gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(), + gatewayIds: devices + .where((e) => e.productType == 'GW') + .map((e) => e.uuid!) + .toList(), ); case 'DL': return DoorLockBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'DL') + .map((e) => e.uuid!) + .toList()); case 'WPS': return WallSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'WPS') + .map((e) => e.uuid!) + .toList()); case 'CPS': return CeilingSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'CPS') + .map((e) => e.uuid!) + .toList(), ); case 'CUR': return CurtainBatchStatusView( - devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'CUR') + .map((e) => e.uuid!) + .toList(), + ); + case 'CUR_2': + return CurtainModuleBatchView( + devicesIds: devices + .where((e) => e.productType == 'AC') + .map((e) => e.uuid!) + .toList(), ); case 'AC': return AcDeviceBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'AC') + .map((e) => e.uuid!) + .toList()); case 'WH': return WaterHEaterBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'WH') + .map((e) => e.uuid!) + .toList(), ); case 'DS': return MainDoorSensorBatchView( - devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'DS') + .map((e) => e.uuid!) + .toList(), ); case 'GD': return GarageDoorBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'GD') + .map((e) => e.uuid!) + .toList(), ); case 'WL': return WaterLeakBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'WL') + .map((e) => e.uuid!) + .toList(), ); case 'PC': return PowerClampBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'PC') + .map((e) => e.uuid!) + .toList(), ); case 'SOS': return SOSBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'SOS') + .map((e) => e.uuid!) + .toList(), ); case 'NCPS': return FlushMountedPresenceSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'NCPS') + .map((e) => e.uuid!) + .toList(), ); default: return const SizedBox(); From 03ba50629427bf2c160b3cf7f63e7ee931a6d862 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:27:15 +0300 Subject: [PATCH 081/105] add bloc nd logic --- .../bloc/batch/curtain_module_batch_bloc.dart | 45 ++++ .../bloc/curtain_module_bloc.dart | 232 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart new file mode 100644 index 00000000..87dd53f7 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'curtain_module_batch_event.dart'; +part 'curtain_module_batch_state.dart'; + +class CurtainModuleBatchBloc + extends Bloc { + final ControlDeviceService controlDeviceService; + StreamSubscription? _firebaseSubscription; + + CurtainModuleBatchBloc(this.controlDeviceService) + : super(CurtainModuleBatchInitial()) { + on(_onFetchAcBatchStatus); + } + + Future _onFetchAcBatchStatus( + CutrainModuleFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleBatchLoadingState()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + status.status.forEach( + (element) => print( + 'this is code ${element.code} - this is value ${element.value}'), + ); + + emit( + CurtainModuleBatchLoadedState( + curtainModuleStatusModel: CurtainModuleStatusModel.fromJson({}), + ), + ); + } catch (e) { + emit(CurtainModuleBatchFailedState(error: e.toString())); + } + } +} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart new file mode 100644 index 00000000..d79380cf --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -0,0 +1,232 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'curtain_module_event.dart'; +part 'curtain_module_state.dart'; + +class CurtainModuleBloc extends Bloc { + final ControlDeviceService controlDeviceService; + StreamSubscription? _firebaseSubscription; + + CurtainModuleBloc(this.controlDeviceService) : super(CurtainModuleInitial()) { + on(_onFetchCurtainModuleStatusEvent); + on(_onSendCurtainPercentToApiEvent); + on(_onOpenCurtainEvent); + on(_onCloseCurtainEvent); + on(_onStopCurtainEvent); + on(_onChangeTimerControlEvent); + on(_onChageCurCalibrationEvent); + on(_onChangeElecMachineryModeEvent); + on(_onChangeControlBackEvent); + on(_onChangeControlBackModeEvent); + on(_onChangeCurtainModuleStatusEvent); + } + + Future _onFetchCurtainModuleStatusEvent( + FetchCurtainModuleStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final result = Map.fromEntries( + status.status.map((element) => MapEntry(element.code, element.value)), + ); + + emit(CurtainModuleStatusLoaded( + curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), + )); + Map statusMap = {}; + final ref = + FirebaseDatabase.instance.ref('device-status/${event.deviceId}'); + final stream = ref.onValue; + + stream.listen((DatabaseEvent DatabaseEvent) async { + if (DatabaseEvent.snapshot.value == null) return; + + Map usersMap = + DatabaseEvent.snapshot.value as Map; + + List statusList = []; + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + statusMap = { + for (final element in statusList) element.code: element.value, + }; + if (!isClosed) { + add( + ChangeCurtainModuleStatusEvent( + deviceId: event.deviceId, + status: CurtainModuleStatusModel.fromJson(statusMap), + ), + ); + } + }); + } + + Future _onChangeCurtainModuleStatusEvent( + ChangeCurtainModuleStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + emit(CurtainModuleStatusLoaded(curtainModuleStatus: event.status)); + } + + Future _onSendCurtainPercentToApiEvent( + SendCurtainPercentToApiEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: event.status, + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to send control command: $e')); + } + } + + Future _onOpenCurtainEvent( + OpenCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'open'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to open curtain: $e')); + } + } + + Future _onCloseCurtainEvent( + CloseCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'close'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to close curtain: $e')); + } + } + + Future _onStopCurtainEvent( + StopCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'stop'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to stop curtain: $e')); + } + } + + Future _onChangeTimerControlEvent( + ChangeTimerControlEvent event, + Emitter emit, + ) async { + try { + if (event.timControl < 10 || event.timControl > 120) { + emit(const CurtainModuleError( + message: 'Timer control value must be between 10 and 120')); + return; + } + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'tr_timecon', + value: event.timControl, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change timer control: $e')); + } + } + + Future _onChageCurCalibrationEvent( + CurCalibrationEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'cur_calibration', value: 'start'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to start calibration: $e')); + } + } + + Future _onChangeElecMachineryModeEvent( + ChangeElecMachineryModeEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'elec_machinery_mode', + value: event.elecMachineryMode, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change mode: $e')); + } + } + + Future _onChangeControlBackEvent( + ChangeControlBackEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'control_back', + value: event.controlBack, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change control back: $e')); + } + } + + Future _onChangeControlBackModeEvent( + ChangeControlBackModeEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'control_back_mode', + value: event.controlBackMode, + ), + ); + } catch (e) { + emit(CurtainModuleError( + message: 'Failed to change control back mode: $e')); + } + } + + @override + Future close() async { + await _firebaseSubscription?.cancel(); + return super.close(); + } +} From eee6a80c50aa58b467aceee07506bedb2f04f1d9 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:27:40 +0300 Subject: [PATCH 082/105] add events and states and models --- .../batch/curtain_module_batch_event.dart | 19 +++ .../batch/curtain_module_batch_state.dart | 28 ++++ .../bloc/curtain_module_event.dart | 132 ++++++++++++++++++ .../bloc/curtain_module_state.dart | 37 +++++ .../models/curtain_module_model.dart | 53 +++++++ 5 files changed, 269 insertions(+) create mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart create mode 100644 lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart create mode 100644 lib/pages/device_managment/curtain_module/models/curtain_module_model.dart diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart new file mode 100644 index 00000000..351773f9 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart @@ -0,0 +1,19 @@ +part of 'curtain_module_batch_bloc.dart'; + +sealed class CurtainModuleBatchEvent extends Equatable { + const CurtainModuleBatchEvent(); + + @override + List get props => []; +} + +class CutrainModuleFetchBatchStatusEvent extends CurtainModuleBatchEvent { + final List devicesIds; + + const CutrainModuleFetchBatchStatusEvent({ + required this.devicesIds, + }); + + @override + List get props => [devicesIds]; +} diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart new file mode 100644 index 00000000..bb8bb7d0 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart @@ -0,0 +1,28 @@ +part of 'curtain_module_batch_bloc.dart'; + +sealed class CurtainModuleBatchState extends Equatable { + const CurtainModuleBatchState(); + + @override + List get props => []; +} + +final class CurtainModuleBatchInitial extends CurtainModuleBatchState {} + +final class CurtainModuleBatchLoadingState extends CurtainModuleBatchState {} + +final class CurtainModuleBatchLoadedState extends CurtainModuleBatchState { + final CurtainModuleStatusModel curtainModuleStatusModel; + const CurtainModuleBatchLoadedState({ + required this.curtainModuleStatusModel, + }); +} + +final class CurtainModuleBatchFailedState extends CurtainModuleBatchState { + final String error; + + const CurtainModuleBatchFailedState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart new file mode 100644 index 00000000..6b0d89ae --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart @@ -0,0 +1,132 @@ +part of 'curtain_module_bloc.dart'; + +sealed class CurtainModuleEvent extends Equatable { + const CurtainModuleEvent(); + + @override + List get props => []; +} + +class FetchCurtainModuleStatusEvent extends CurtainModuleEvent { + final String deviceId; + const FetchCurtainModuleStatusEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class SendCurtainPercentToApiEvent extends CurtainModuleEvent { + final String deviceId; + final Status status; + + const SendCurtainPercentToApiEvent({ + required this.deviceId, + required this.status, + }); + + @override + List get props => [deviceId, status]; +} + +class OpenCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const OpenCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class CloseCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const CloseCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class StopCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const StopCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class ChangeTimerControlEvent extends CurtainModuleEvent { + final String deviceId; + final int timControl; + + const ChangeTimerControlEvent({ + required this.deviceId, + required this.timControl, + }); + + @override + List get props => [deviceId, timControl]; +} + +class CurCalibrationEvent extends CurtainModuleEvent { + final String deviceId; + + const CurCalibrationEvent({ + required this.deviceId, + }); + + @override + List get props => [deviceId]; +} + +class ChangeElecMachineryModeEvent extends CurtainModuleEvent { + final String deviceId; + final String elecMachineryMode; + + const ChangeElecMachineryModeEvent({ + required this.deviceId, + required this.elecMachineryMode, + }); + + @override + List get props => [deviceId, elecMachineryMode]; +} + +class ChangeControlBackEvent extends CurtainModuleEvent { + final String deviceId; + final String controlBack; + + const ChangeControlBackEvent({ + required this.deviceId, + required this.controlBack, + }); + + @override + List get props => [deviceId, controlBack]; +} + +class ChangeControlBackModeEvent extends CurtainModuleEvent { + final String deviceId; + final String controlBackMode; + + const ChangeControlBackModeEvent({ + required this.deviceId, + required this.controlBackMode, + }); + + @override + List get props => [deviceId, controlBackMode]; +} + +class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent { + final String deviceId; + final CurtainModuleStatusModel status; + + const ChangeCurtainModuleStatusEvent({ + required this.deviceId, + required this.status, + }); + + @override + List get props => [deviceId, status]; +} \ No newline at end of file diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart new file mode 100644 index 00000000..02ef9279 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart @@ -0,0 +1,37 @@ +part of 'curtain_module_bloc.dart'; + +sealed class CurtainModuleState extends Equatable { + const CurtainModuleState(); + + @override + List get props => []; +} + +class CurtainModuleInitial extends CurtainModuleState {} + +class CurtainModuleLoading extends CurtainModuleState {} + +class CurtainModuleError extends CurtainModuleState { + final String message; + const CurtainModuleError({required this.message}); + + @override + List get props => [message]; +} + +class CurtainModuleStatusLoaded extends CurtainModuleState { + final CurtainModuleStatusModel curtainModuleStatus; + + const CurtainModuleStatusLoaded({required this.curtainModuleStatus}); + + @override + List get props => [curtainModuleStatus]; +} +class CurtainModuleStatusUpdated extends CurtainModuleState { + final CurtainModuleStatusModel curtainModuleStatus; + + const CurtainModuleStatusUpdated({required this.curtainModuleStatus}); + + @override + List get props => [curtainModuleStatus]; +} diff --git a/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart b/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart new file mode 100644 index 00000000..0b6d23fb --- /dev/null +++ b/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart @@ -0,0 +1,53 @@ + +enum CurtainModuleControl { + open, + close, + stop, +} + +// enum CurtainControlBackMode { +// foward, +// backward, +// } + +class CurtainModuleStatusModel { + CurtainModuleControl control; + int percentControl; + String curCalibration; + // CurtainControlBackMode controlBackmode; + int trTimeControl; + String elecMachineryMode; + String controlBack; + CurtainModuleStatusModel({ + required this.control, + required this.percentControl, + required this.curCalibration, + // required this.controlBackmode, + required this.trTimeControl, + required this.controlBack, + required this.elecMachineryMode, + }); + factory CurtainModuleStatusModel.zero() => CurtainModuleStatusModel( + control: CurtainModuleControl.stop, + percentControl: 0, + // controlBackmode: CurtainControlBackMode.foward, + curCalibration: '', + trTimeControl: 0, + controlBack: '', + elecMachineryMode: '', + ); + + factory CurtainModuleStatusModel.fromJson(Map json) { + return CurtainModuleStatusModel( + control: CurtainModuleControl.values.firstWhere( + (e) => e.toString() == json['control'] as String, + orElse: () => CurtainModuleControl.stop, + ), + percentControl: json['percent_control'] as int? ?? 0, + curCalibration: json['cur_calibration'] as String? ?? '', + trTimeControl: json['tr_timecon'] as int? ?? 0, + elecMachineryMode: json['elec_machinery_mode'] as String? ?? '', + controlBack: json['control_back'] as String? ?? '', + ); + } +} From 4e9bcbdcea3fab23c024d64196c7b2f069137bae Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:28:15 +0300 Subject: [PATCH 083/105] build UI and integrate with back --- .../view/curtain_module_batch.dart | 71 +++++++ .../view/curtain_module_items.dart | 98 +++++++++ .../widgets/accurate_calibrating_dialog.dart | 48 +++++ .../widgets/accurate_calibration_dialog.dart | 40 ++++ .../widgets/accurate_dialog_widget.dart | 97 +++++++++ .../widgets/calibrate_completed_dialog.dart | 77 +++++++ .../widgets/curtain_action_widget.dart | 41 ++++ .../widgets/curtain_movment_widget.dart | 190 ++++++++++++++++++ .../widgets/normal_text_body_for_dialog.dart | 42 ++++ .../widgets/number_input_textfield.dart | 27 +++ .../widgets/pref_revers_card_widget.dart | 79 ++++++++ .../widgets/prefrences_dialog.dart | 137 +++++++++++++ .../widgets/quick_calibrating_dialog.dart | 122 +++++++++++ .../widgets/quick_calibration_dialog.dart | 44 ++++ lib/utils/constants/assets.dart | 6 +- 15 files changed, 1118 insertions(+), 1 deletion(-) create mode 100644 lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart create mode 100644 lib/pages/device_managment/curtain_module/view/curtain_module_items.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart create mode 100644 lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart new file mode 100644 index 00000000..0e0dfdc3 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CurtainModuleBatchView extends StatelessWidget { + final List devicesIds; + const CurtainModuleBatchView({ + super.key, + required this.devicesIds, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainModuleBatchBloc(RemoteControlDeviceService()) + ..add(CutrainModuleFetchBatchStatusEvent(devicesIds: devicesIds)), + child: _buildStatusControls(context), + ); + } + + Widget _buildStatusControls(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ControlCurtainMovementWidget( + deviceId: devicesIds.first, + ), + const SizedBox( + height: 10, + ), + SizedBox( + height: 120, + width: 350, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: IconNameStatusContainer( + isFullIcon: false, + name: 'Factory Reset', + icon: Assets.factoryReset, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + ), + Expanded( + child: IconNameStatusContainer( + isFullIcon: false, + name: 'Firmware Update', + icon: Assets.firmware, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart new file mode 100644 index 00000000..95366e49 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + const CurtainModuleItems({ + super.key, + required this.deviceId, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainModuleBloc(RemoteControlDeviceService()) + ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), + child: _buildStatusControls(context), + ); + } + + Widget _buildStatusControls(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ControlCurtainMovementWidget( + deviceId: deviceId, + ), + const SizedBox( + height: 10, + ), + SizedBox( + height: 120, + width: 350, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: IconNameStatusContainer( + isFullIcon: false, + name: 'Schedules', + icon: Assets.schedule, + onTap: () {}, + status: false, + textColor: ColorsManager.blackColor, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is CurtainModuleStatusLoaded) { + return IconNameStatusContainer( + isFullIcon: false, + name: 'Preferences', + icon: Assets.preferences, + onTap: () => showDialog( + context: context, + builder: (_) => BlocProvider.value( + value: context.watch(), + child: CurtainModulePrefrencesDialog( + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, + ), + ), + ), + status: false, + textColor: ColorsManager.blackColor, + ); + } else { + return const SizedBox(); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart new file mode 100644 index 00000000..54107420 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; + +class AccurteCalibratingDialog extends StatelessWidget { + final String deviceId; + final BuildContext parentContext; + const AccurteCalibratingDialog({ + super.key, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Calibrating', + body: const NormalTextBodyForDialog( + title: '', + step1: + '1. Click Close Button to make the Curtain run to Full Close and Position.', + step2: '2. click Next to complete the Calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + parentContext.read().add( + CurCalibrationEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => CalibrateCompletedDialog( + parentContext: parentContext, + deviceId: deviceId, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart new file mode 100644 index 00000000..a9d1b010 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; + +class AccurateCalibrationDialog extends StatelessWidget { + final String deviceId; + final BuildContext parentContext; + const AccurateCalibrationDialog({ + super.key, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Accurate Calibration', + body: const NormalTextBodyForDialog( + title: 'Prepare Calibration:', + step1: '1. Run The Curtain to the Fully Open Position,and pause.', + step2: '2. click Next to Start accurate calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => AccurteCalibratingDialog( + deviceId: deviceId, + parentContext: parentContext, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart new file mode 100644 index 00000000..5be376ae --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AccurateDialogWidget extends StatelessWidget { + final String title; + final Widget body; + final void Function() leftOnTap; + final void Function() rightOnTap; + const AccurateDialogWidget({ + super.key, + required this.title, + required this.body, + required this.leftOnTap, + required this.rightOnTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 300, + width: 400, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.blueColor, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + Padding( + padding: const EdgeInsets.all(10), + child: body, + ), + const SizedBox(height: 20), + const Spacer(), + const Divider(), + Row( + children: [ + Expanded( + child: InkWell( + onTap: leftOnTap, + child: Container( + height: 60, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.grayBorder), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: rightOnTap, + child: Container( + height: 60, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Next', + style: TextStyle( + color: ColorsManager.blueColor, + ), + ), + ), + ), + ) + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart new file mode 100644 index 00000000..9b2b5ea9 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CalibrateCompletedDialog extends StatelessWidget { + final BuildContext parentContext; + final String deviceId; + const CalibrateCompletedDialog({ + super.key, + required this.parentContext, + required this.deviceId, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: SizedBox( + height: 250, + width: 400, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Text( + 'Calibration Completed', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.blueColor, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + const Icon( + Icons.check_circle, + size: 100, + color: ColorsManager.blueColor, + ), + const Spacer(), + const Divider( + indent: 10, + endIndent: 10, + ), + InkWell( + onTap: () { + parentContext.read().add( + FetchCurtainModuleStatusEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + Navigator.of(parentContext).pop(); + }, + child: Container( + height: 40, + width: double.infinity, + alignment: Alignment.center, + child: const Text( + 'Close', + style: TextStyle( + color: ColorsManager.grayBorder, + ), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart new file mode 100644 index 00000000..8c2ff81c --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CurtainActionWidget extends StatelessWidget { + final String icon; + final void Function() onTap; + const CurtainActionWidget({ + super.key, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.whiteColors, + child: ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.graysColor, + child: SvgPicture.asset( + icon, + width: 35, + height: 35, + fit: BoxFit.contain, + ), + ), + ), + )), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart new file mode 100644 index 00000000..076c5a9c --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ControlCurtainMovementWidget extends StatelessWidget { + final String deviceId; + const ControlCurtainMovementWidget({ + super.key, + required this.deviceId, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 550, + child: DeviceControlsContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CurtainActionWidget( + icon: Assets.openCurtain, + onTap: () { + context.read().add( + OpenCurtainEvent(deviceId: deviceId), + ); + }, + ), + const SizedBox( + width: 30, + ), + CurtainActionWidget( + icon: Assets.pauseCurtain, + onTap: () { + context.read().add( + StopCurtainEvent(deviceId: deviceId), + ); + }, + ), + const SizedBox( + width: 30, + ), + CurtainActionWidget( + icon: Assets.closeCurtain, + onTap: () { + context.read().add( + CloseCurtainEvent(deviceId: deviceId), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleError) { + return Center( + child: Text( + state.message, + style: const TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } else if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator( + color: ColorsManager.minBlueDot, + ), + ); + } else if (state is CurtainModuleInitial) { + return const Center( + child: Text( + 'No data available', + style: TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } else if (state is CurtainModuleStatusLoaded) { + return CurtainSliderWidget( + status: state.curtainModuleStatus, + deviceId: deviceId, + ); + } else { + return const Center( + child: Text( + 'Unknown state', + style: TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } + }, + ) + ], + ), + ), + ); + } +} + +class CurtainSliderWidget extends StatefulWidget { + final CurtainModuleStatusModel status; + final String deviceId; + + const CurtainSliderWidget({ + super.key, + required this.status, + required this.deviceId, + }); + + @override + State createState() => _CurtainSliderWidgetState(); +} + +class _CurtainSliderWidgetState extends State { + double? _localValue; // For temporary drag state + + @override + Widget build(BuildContext context) { + // If user is dragging, use local value. Otherwise, use Firebase-synced state + final double currentSliderValue = + _localValue ?? widget.status.percentControl / 100; + + return Column( + children: [ + Text( + '${(currentSliderValue * 100).round()}%', + style: const TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + Slider( + value: currentSliderValue, + min: 0, + max: 1, + divisions: 10, // 10% step + activeColor: ColorsManager.minBlueDot, + thumbColor: ColorsManager.primaryColor, + inactiveColor: ColorsManager.whiteColors, + + // Start dragging — use local control + onChangeStart: (_) { + setState(() { + _localValue = currentSliderValue; + }); + }, + + // While dragging — update temporary value + onChanged: (value) { + final steppedValue = (value * 10).roundToDouble() / 10; + setState(() { + _localValue = steppedValue; + }); + }, + + // On release — send API and return to Firebase-controlled state + onChangeEnd: (value) { + final int targetPercent = (value * 100).round(); + + // Dispatch API call + context.read().add( + SendCurtainPercentToApiEvent( + deviceId: widget.deviceId, + status: Status( + code: 'percent_control', + value: targetPercent, + ), + ), + ); + + // Revert back to Firebase-synced stream + setState(() { + _localValue = null; + }); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart new file mode 100644 index 00000000..8818cb7b --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NormalTextBodyForDialog extends StatelessWidget { + final String title; + final String step1; + final String step2; + + const NormalTextBodyForDialog({ + super.key, + required this.title, + required this.step1, + required this.step2, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ), + Text( + step1, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ), + Text( + step2, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ) + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart new file mode 100644 index 00000000..ea95f838 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NumberInputField extends StatelessWidget { + final TextEditingController controller; + + const NumberInputField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: const TextStyle( + fontSize: 20, + color: ColorsManager.blackColor, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart new file mode 100644 index 00000000..81912e80 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class PrefReversCardWidget extends StatelessWidget { + final void Function() onTap; + final String title; + final String body; + const PrefReversCardWidget({ + super.key, + required this.title, + required this.body, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return DefaultContainer( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 8, + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayBorder, + fontSize: 15, + ), + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 2, + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(10), + right: Radius.circular(10)), + border: Border.all(color: ColorsManager.grayBorder)), + child: SvgPicture.asset( + Assets.reverseArrows, + height: 15, + ), + ), + ), + ) + ], + ), + SizedBox( + width: 100, + child: Text( + body, + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart new file mode 100644 index 00000000..c5fcf4ba --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class CurtainModulePrefrencesDialog extends StatelessWidget { + final CurtainModuleStatusModel curtainModuleStatusModel; + final String deviceId; + + const CurtainModulePrefrencesDialog({ + super.key, + required this.curtainModuleStatusModel, + required this.deviceId, + }); + + @override + Widget build(_) { + return AlertDialog( + backgroundColor: ColorsManager.CircleImageBackground, + contentPadding: const EdgeInsets.all(30), + title: const Center( + child: Text( + 'Preferences', + style: TextStyle( + color: ColorsManager.blueColor, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + )), + content: BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is CurtainModuleStatusLoaded) { + return SizedBox( + height: 300, + width: 400, + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.5, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + children: [ + PrefReversCardWidget( + title: state.curtainModuleStatus.controlBack, + body: 'Motor Steering', + onTap: () { + context.read().add( + ChangeControlBackEvent( + deviceId: deviceId, + controlBack: + state.curtainModuleStatus.controlBack == + 'forward' + ? 'back' + : 'forward', + ), + ); + }, + ), + PrefReversCardWidget( + title: state.curtainModuleStatus.elecMachineryMode, + body: 'Motor Mode', + onTap: () => context.read().add( + ChangeElecMachineryModeEvent( + deviceId: deviceId, + elecMachineryMode: + state.curtainModuleStatus.elecMachineryMode == + 'dry_contact' + ? 'strong_power' + : 'dry_contact', + ), + ), + ), + DefaultContainer( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => AccurateCalibrationDialog( + deviceId: deviceId, + parentContext: context, + ), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Accurte Calibration', + style: TextStyle( + fontSize: 18, + color: ColorsManager.blackColor, + )), + ], + ), + ), + ), + DefaultContainer( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => QuickCalibrationDialog( + timControl: state.curtainModuleStatus.trTimeControl, + deviceId: deviceId, + parentContext: context), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Quick Calibration', + style: TextStyle( + fontSize: 18, + color: ColorsManager.blackColor, + )), + ], + ), + ), + ), + ], + ), + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart new file mode 100644 index 00000000..0b86c96e --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/number_input_textfield.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class QuickCalibratingDialog extends StatefulWidget { + final int timControl; + final String deviceId; + final BuildContext parentContext; + const QuickCalibratingDialog({ + super.key, + required this.timControl, + required this.deviceId, + required this.parentContext, + }); + + @override + State createState() => _QuickCalibratingDialogState(); +} + +class _QuickCalibratingDialogState extends State { + late TextEditingController _controller; + String? _errorText; + + void _onRightTap() { + final value = int.tryParse(_controller.text); + + if (value == null || value < 10 || value > 120) { + setState(() { + _errorText = 'Number should be between 10 and 120'; + }); + return; + } + + setState(() { + _errorText = null; + }); + widget.parentContext.read().add( + ChangeTimerControlEvent( + deviceId: widget.deviceId, + timControl: value, + ), + ); + Navigator.of(widget.parentContext).pop(); + showDialog( + context: widget.parentContext, + builder: (_) => CalibrateCompletedDialog( + parentContext: widget.parentContext, + deviceId: widget.deviceId, + ), + ); + } + + @override + void initState() { + _controller = TextEditingController(text: widget.timControl.toString()); + super.initState(); + } + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Calibrating', + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '1. please Enter the Travel Time:', + style: TextStyle(color: ColorsManager.grayBorder), + ), + const SizedBox(height: 10), + Container( + width: 150, + height: 40, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: NumberInputField(controller: _controller), + ), + const Expanded( + child: Text( + 'seconds', + style: TextStyle( + fontSize: 15, + color: ColorsManager.blueColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + if (_errorText != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _errorText!, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 14, + ), + ), + ), + ], + ), + leftOnTap: () => Navigator.of(widget.parentContext).pop(), + rightOnTap: _onRightTap, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart new file mode 100644 index 00000000..803d904f --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart'; + +class QuickCalibrationDialog extends StatelessWidget { + final int timControl; + final String deviceId; + final BuildContext parentContext; + const QuickCalibrationDialog({ + super.key, + required this.timControl, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Quick Calibration', + body: const NormalTextBodyForDialog( + title: 'Prepare Calibration:', + step1: + '1. Confirm that the curtain is in the fully closed and suspended state.', + step2: '2. click Next to Start calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => QuickCalibratingDialog( + timControl: timControl, + deviceId: deviceId, + parentContext: parentContext, + ), + ); + }, + ), + ); + } +} diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index dfc0b394..331a4285 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -124,7 +124,11 @@ class Assets { //assets/icons/AC.svg static const String ac = "assets/icons/AC.svg"; //assets/icons/Curtain.svg - static const String curtain = "assets/icons/Curtain.svg"; + static const String curtain = 'assets/icons/Curtain.svg'; + static const String openCurtain = 'assets/icons/open_curtain.svg'; + static const String pauseCurtain = 'assets/icons/pause_curtain.svg'; + static const String closeCurtain = 'assets/icons/close_curtain.svg'; + static const String reverseArrows = 'assets/icons/reverse_arrows.svg'; //assets/icons/doorLock.svg static const String doorLock = "assets/icons/doorLock.svg"; //assets/icons/Gateway.svg From 0b372e1ed8746218c950cda7dc72521618510e0f Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 16:49:11 +0300 Subject: [PATCH 084/105] use read instead of watch --- .../curtain_module/view/curtain_module_items.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 95366e49..15175bd5 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -71,7 +71,7 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { onTap: () => showDialog( context: context, builder: (_) => BlocProvider.value( - value: context.watch(), + value: context.read(), child: CurtainModulePrefrencesDialog( deviceId: deviceId, curtainModuleStatusModel: From f43826a82489e88581b2850e87c4fb3ad5053671 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Wed, 25 Jun 2025 17:07:59 +0300 Subject: [PATCH 085/105] now it is rendering the chages for motors and control back --- .../curtain_module/view/curtain_module_items.dart | 13 ++++++------- .../curtain_module/widgets/prefrences_dialog.dart | 4 +++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 15175bd5..22ccf90e 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -70,13 +70,12 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { icon: Assets.preferences, onTap: () => showDialog( context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: CurtainModulePrefrencesDialog( - deviceId: deviceId, - curtainModuleStatusModel: - state.curtainModuleStatus, - ), + builder: (_) => CurtainModulePrefrencesDialog( + curtainModuleBloc: + context.watch(), + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, ), ), status: false, diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart index c5fcf4ba..bf500792 100644 --- a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -11,11 +11,12 @@ import 'package:syncrow_web/web_layout/default_container.dart'; class CurtainModulePrefrencesDialog extends StatelessWidget { final CurtainModuleStatusModel curtainModuleStatusModel; final String deviceId; - + final CurtainModuleBloc curtainModuleBloc; const CurtainModulePrefrencesDialog({ super.key, required this.curtainModuleStatusModel, required this.deviceId, + required this.curtainModuleBloc, }); @override @@ -33,6 +34,7 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { ), )), content: BlocBuilder( + bloc: curtainModuleBloc, builder: (context, state) { if (state is CurtainModuleLoading) { return const Center( From 11e285340376b90f0d169b8eb31e66219a8ddb64 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 11:56:43 +0300 Subject: [PATCH 086/105] Enhance AnalyticsDeviceDropdown to show loading indicator during loading state. --- .../widgets/analytics_device_dropdown.dart | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart index f7b33309..055e9675 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart @@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget { ), ), child: Visibility( - visible: state.devices.isNotEmpty, - replacement: _buildNoDevicesFound(context), - child: _buildDevicesDropdown(context, state), + visible: state.status != AnalyticsDevicesStatus.loading, + replacement: _buildLoadingIndicator(), + child: Visibility( + visible: state.devices.isNotEmpty, + replacement: _buildNoDevicesFound(context), + child: _buildDevicesDropdown(context, state), + ), ), ); }, ); } + Widget _buildLoadingIndicator() { + return const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 3), + ), + ); + } + static const _defaultPadding = EdgeInsetsDirectional.symmetric( horizontal: 20, vertical: 2, From 7fda564ee43256336723fec9b0e1fc2bf059a08c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 14:07:24 +0300 Subject: [PATCH 087/105] hotfixes. --- lib/pages/analytics/models/occupancy_heat_map_model.dart | 4 +++- .../modules/air_quality/widgets/aqi_type_dropdown.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 73e7d5d7..cd332745 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -19,7 +19,9 @@ class OccupancyHeatMapModel extends Equatable { eventDate: DateTime.parse( json['event_date'] as String? ?? '${DateTime.now()}', ), - countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0, + countTotalPresenceDetected: num.parse( + json['count_total_presence_detected']?.toString() ?? '900', + ).toInt(), ); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 457bf610..6640c717 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -6,7 +6,7 @@ enum AqiType { aqi('AQI', '', 'aqi'), pm25('PM2.5', 'µg/m³', 'pm25'), pm10('PM10', 'µg/m³', 'pm10'), - hcho('HCHO', 'mg/m³', 'cho2'), + hcho('HCHO', 'mg/m³', 'ch2o'), tvoc('TVOC', 'mg/m³', 'voc'), co2('CO2', 'ppm', 'co2'); From 475462301f8ff72b2b666d8ddadbcf25700cbab9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 15:29:11 +0300 Subject: [PATCH 088/105] manually parse event date for heatmap date object. --- .../analytics/models/occupancy_heat_map_model.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index cd332745..60d1a453 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,13 +14,16 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map json) { + final eventDate = json['event_date'] as String? ?? '${DateTime.now()}'; + final year = eventDate.split('-')[0]; + final month = eventDate.split('-')[1]; + final day = eventDate.split('-')[2]; + return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime.parse( - json['event_date'] as String? ?? '${DateTime.now()}', - ), + eventDate: DateTime(int.parse(year), int.parse(month), int.parse(day)), countTotalPresenceDetected: num.parse( - json['count_total_presence_detected']?.toString() ?? '900', + json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), ); } From 396ce3dad8c55bd1a5757fc3de0f85dd24af0c17 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 16:26:39 +0300 Subject: [PATCH 089/105] now batch is working --- .../device_managment/ac/bloc/ac_bloc.dart | 15 ++- .../helper/route_controls_based_code.dart | 2 +- .../bloc/batch/curtain_module_batch_bloc.dart | 45 ------- .../batch/curtain_module_batch_event.dart | 19 --- .../batch/curtain_module_batch_state.dart | 28 ----- .../bloc/curtain_module_bloc.dart | 118 +++++++++++++++++- .../bloc/curtain_module_event.dart | 63 +++++++++- .../view/curtain_module_batch.dart | 57 +++++---- .../view/curtain_module_items.dart | 22 ++-- .../widgets/curtain_movment_widget.dart | 75 +++++++---- .../shared/device_batch_control_dialog.dart | 5 +- .../batch_control_devices_service.dart | 3 +- 12 files changed, 294 insertions(+), 158 deletions(-) delete mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart delete mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart delete mode 100644 lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index af5a7b0a..9a8e18a2 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -45,7 +45,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -82,10 +83,12 @@ class AcBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); + statusList + .add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = + AcStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } @@ -129,8 +132,10 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index cc09260e..08bca73c 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -213,7 +213,7 @@ mixin RouteControlsBasedCode { case 'CUR_2': return CurtainModuleBatchView( devicesIds: devices - .where((e) => e.productType == 'AC') + .where((e) => e.productType == 'CUR_2') .map((e) => e.uuid!) .toList(), ); diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart deleted file mode 100644 index 87dd53f7..00000000 --- a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:firebase_database/firebase_database.dart'; -import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; -import 'package:syncrow_web/services/devices_mang_api.dart'; - -part 'curtain_module_batch_event.dart'; -part 'curtain_module_batch_state.dart'; - -class CurtainModuleBatchBloc - extends Bloc { - final ControlDeviceService controlDeviceService; - StreamSubscription? _firebaseSubscription; - - CurtainModuleBatchBloc(this.controlDeviceService) - : super(CurtainModuleBatchInitial()) { - on(_onFetchAcBatchStatus); - } - - Future _onFetchAcBatchStatus( - CutrainModuleFetchBatchStatusEvent event, - Emitter emit, - ) async { - emit(CurtainModuleBatchLoadingState()); - try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - status.status.forEach( - (element) => print( - 'this is code ${element.code} - this is value ${element.value}'), - ); - - emit( - CurtainModuleBatchLoadedState( - curtainModuleStatusModel: CurtainModuleStatusModel.fromJson({}), - ), - ); - } catch (e) { - emit(CurtainModuleBatchFailedState(error: e.toString())); - } - } -} diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart deleted file mode 100644 index 351773f9..00000000 --- a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_event.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of 'curtain_module_batch_bloc.dart'; - -sealed class CurtainModuleBatchEvent extends Equatable { - const CurtainModuleBatchEvent(); - - @override - List get props => []; -} - -class CutrainModuleFetchBatchStatusEvent extends CurtainModuleBatchEvent { - final List devicesIds; - - const CutrainModuleFetchBatchStatusEvent({ - required this.devicesIds, - }); - - @override - List get props => [devicesIds]; -} diff --git a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart b/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart deleted file mode 100644 index bb8bb7d0..00000000 --- a/lib/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_state.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of 'curtain_module_batch_bloc.dart'; - -sealed class CurtainModuleBatchState extends Equatable { - const CurtainModuleBatchState(); - - @override - List get props => []; -} - -final class CurtainModuleBatchInitial extends CurtainModuleBatchState {} - -final class CurtainModuleBatchLoadingState extends CurtainModuleBatchState {} - -final class CurtainModuleBatchLoadedState extends CurtainModuleBatchState { - final CurtainModuleStatusModel curtainModuleStatusModel; - const CurtainModuleBatchLoadedState({ - required this.curtainModuleStatusModel, - }); -} - -final class CurtainModuleBatchFailedState extends CurtainModuleBatchState { - final String error; - - const CurtainModuleBatchFailedState({required this.error}); - - @override - List get props => [error]; -} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart index d79380cf..c3cd0b92 100644 --- a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -3,7 +3,9 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -12,9 +14,13 @@ part 'curtain_module_state.dart'; class CurtainModuleBloc extends Bloc { final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; StreamSubscription? _firebaseSubscription; - CurtainModuleBloc(this.controlDeviceService) : super(CurtainModuleInitial()) { + CurtainModuleBloc({ + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CurtainModuleInitial()) { on(_onFetchCurtainModuleStatusEvent); on(_onSendCurtainPercentToApiEvent); on(_onOpenCurtainEvent); @@ -26,6 +32,13 @@ class CurtainModuleBloc extends Bloc { on(_onChangeControlBackEvent); on(_onChangeControlBackModeEvent); on(_onChangeCurtainModuleStatusEvent); + //batch + on(_onFetchCurtainModuleBatchStatus); + on(_onSendCurtainBatchPercentToApiEvent); + on(_onOpenCurtainBatchEvent); + on(_onCloseCurtainBatchEvent); + on(_onStopCurtainBatchEvent); + on(_onFactoryReset); } Future _onFetchCurtainModuleStatusEvent( @@ -224,6 +237,109 @@ class CurtainModuleBloc extends Bloc { } } + FutureOr _onFetchCurtainModuleBatchStatus( + CurtainModuleFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + + final result = Map.fromEntries( + status.status.map((element) => MapEntry(element.code, element.value)), + ); + + emit(CurtainModuleStatusLoaded( + curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), + )); + } catch (e) { + emit(CurtainModuleError(message: e.toString())); + } + } + + Future _onSendCurtainBatchPercentToApiEvent( + SendCurtainBatchPercentToApiEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: event.status.code, + value: event.status.value, + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to send control command: $e')); + } + } + + Future _onOpenCurtainBatchEvent( + OpenCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'open', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to open curtain: $e')); + } + } + + Future _onCloseCurtainBatchEvent( + CloseCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'close', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to close curtain: $e')); + } + } + + Future _onStopCurtainBatchEvent( + StopCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'stop', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to stop curtain: $e')); + } + } + + Future _onFactoryReset( + CurtainModuleFactoryReset event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const CurtainModuleError(message: 'Failed')); + } else { + add( + FetchCurtainModuleStatusEvent(deviceId: event.deviceId), + ); + } + } catch (e) { + emit(CurtainModuleError(message: e.toString())); + } + } + @override Future close() async { await _firebaseSubscription?.cancel(); diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart index 6b0d89ae..4eec030d 100644 --- a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart @@ -129,4 +129,65 @@ class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent { @override List get props => [deviceId, status]; -} \ No newline at end of file +} + +///batch +class CurtainModuleFetchBatchStatusEvent extends CurtainModuleEvent { + final List devicesIds; + + const CurtainModuleFetchBatchStatusEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class SendCurtainBatchPercentToApiEvent extends CurtainModuleEvent { + final List devicesId; + final Status status; + + const SendCurtainBatchPercentToApiEvent({ + required this.devicesId, + required this.status, + }); + + @override + List get props => [devicesId, status]; +} + +class OpenCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const OpenCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class CloseCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const CloseCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class StopCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const StopCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class CurtainModuleFactoryReset extends CurtainModuleEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const CurtainModuleFactoryReset( + {required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart index 0e0dfdc3..bd28cd8a 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/batch/curtain_module_batch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -17,8 +20,10 @@ class CurtainModuleBatchView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainModuleBatchBloc(RemoteControlDeviceService()) - ..add(CutrainModuleFetchBatchStatusEvent(devicesIds: devicesIds)), + create: (context) => CurtainModuleBloc( + controlDeviceService: RemoteControlDeviceService(), + batchControlDevicesService: RemoteBatchControlDevicesService()) + ..add(CurtainModuleFetchBatchStatusEvent(devicesIds)), child: _buildStatusControls(context), ); } @@ -30,37 +35,41 @@ class CurtainModuleBatchView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ControlCurtainMovementWidget( - deviceId: devicesIds.first, + devicesId: devicesIds, ), const SizedBox( height: 10, ), SizedBox( height: 120, - width: 350, + // width: 350, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: IconNameStatusContainer( - isFullIcon: false, - name: 'Factory Reset', - icon: Assets.factoryReset, - onTap: () {}, - status: false, - textColor: ColorsManager.blackColor, - ), - ), - Expanded( - child: IconNameStatusContainer( - isFullIcon: false, - name: 'Firmware Update', - icon: Assets.firmware, - onTap: () {}, - status: false, - textColor: ColorsManager.blackColor, - ), + // Expanded( + // child: + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CurtainModuleFactoryReset( + deviceId: devicesIds.first, + factoryReset: + FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, ), + // ), + // Expanded( + // child: IconNameStatusContainer( + // isFullIcon: false, + // name: 'Firmware Update', + // icon: Assets.firmware, + // onTap: () {}, + // status: false, + // textColor: ColorsManager.blackColor, + // ), + // ) ], ), ), diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 22ccf90e..198c8713 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -19,7 +20,9 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainModuleBloc(RemoteControlDeviceService()) + create: (context) => CurtainModuleBloc( + controlDeviceService: RemoteControlDeviceService(), + batchControlDevicesService: RemoteBatchControlDevicesService()) ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), child: _buildStatusControls(context), ); @@ -32,7 +35,7 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { crossAxisAlignment: CrossAxisAlignment.start, children: [ ControlCurtainMovementWidget( - deviceId: deviceId, + devicesId: [deviceId], ), const SizedBox( height: 10, @@ -70,12 +73,15 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { icon: Assets.preferences, onTap: () => showDialog( context: context, - builder: (_) => CurtainModulePrefrencesDialog( - curtainModuleBloc: - context.watch(), - deviceId: deviceId, - curtainModuleStatusModel: - state.curtainModuleStatus, + builder: (_) => BlocProvider.value( + value: context.read(), + child: CurtainModulePrefrencesDialog( + curtainModuleBloc: + context.watch(), + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, + ), ), ), status: false, diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart index 076c5a9c..e98ff11d 100644 --- a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart @@ -9,10 +9,10 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class ControlCurtainMovementWidget extends StatelessWidget { - final String deviceId; + final List devicesId; const ControlCurtainMovementWidget({ super.key, - required this.deviceId, + required this.devicesId, }); @override @@ -26,9 +26,15 @@ class ControlCurtainMovementWidget extends StatelessWidget { CurtainActionWidget( icon: Assets.openCurtain, onTap: () { - context.read().add( - OpenCurtainEvent(deviceId: deviceId), - ); + if (devicesId.length == 1) { + context.read().add( + OpenCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + OpenCurtainBatchEvent(devicesId: devicesId), + ); + } }, ), const SizedBox( @@ -37,9 +43,15 @@ class ControlCurtainMovementWidget extends StatelessWidget { CurtainActionWidget( icon: Assets.pauseCurtain, onTap: () { - context.read().add( - StopCurtainEvent(deviceId: deviceId), - ); + if (devicesId.length == 1) { + context.read().add( + StopCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + StopCurtainBatchEvent(devicesId: devicesId), + ); + } }, ), const SizedBox( @@ -48,9 +60,15 @@ class ControlCurtainMovementWidget extends StatelessWidget { CurtainActionWidget( icon: Assets.closeCurtain, onTap: () { - context.read().add( - CloseCurtainEvent(deviceId: deviceId), - ); + if (devicesId.length == 1) { + context.read().add( + CloseCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + CloseCurtainBatchEvent(devicesId: devicesId), + ); + } }, ), BlocBuilder( @@ -84,7 +102,7 @@ class ControlCurtainMovementWidget extends StatelessWidget { } else if (state is CurtainModuleStatusLoaded) { return CurtainSliderWidget( status: state.curtainModuleStatus, - deviceId: deviceId, + devicesId: devicesId, ); } else { return const Center( @@ -108,12 +126,12 @@ class ControlCurtainMovementWidget extends StatelessWidget { class CurtainSliderWidget extends StatefulWidget { final CurtainModuleStatusModel status; - final String deviceId; + final List devicesId; const CurtainSliderWidget({ super.key, required this.status, - required this.deviceId, + required this.devicesId, }); @override @@ -167,16 +185,27 @@ class _CurtainSliderWidgetState extends State { onChangeEnd: (value) { final int targetPercent = (value * 100).round(); - // Dispatch API call - context.read().add( - SendCurtainPercentToApiEvent( - deviceId: widget.deviceId, - status: Status( - code: 'percent_control', - value: targetPercent, + if (widget.devicesId.length == 1) { + context.read().add( + SendCurtainPercentToApiEvent( + deviceId: widget.devicesId.first, + status: Status( + code: 'percent_control', + value: targetPercent, + ), ), - ), - ); + ); + } else { + context.read().add( + SendCurtainBatchPercentToApiEvent( + devicesId: widget.devicesId, + status: Status( + code: 'percent_control', + value: targetPercent, + ), + ), + ); + } // Revert back to Firebase-synced stream setState(() { diff --git a/lib/pages/device_managment/shared/device_batch_control_dialog.dart b/lib/pages/device_managment/shared/device_batch_control_dialog.dart index f2dc68f5..c7ea6c71 100644 --- a/lib/pages/device_managment/shared/device_batch_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_batch_control_dialog.dart @@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCode { +class DeviceBatchControlDialog extends StatelessWidget + with RouteControlsBasedCode { final List devices; const DeviceBatchControlDialog({super.key, required this.devices}); @@ -18,7 +19,7 @@ class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCo borderRadius: BorderRadius.circular(20), ), child: SizedBox( - width: devices.length < 2 ? 500 : 800, + width: devices.length < 2 ? 600 : 800, // height: context.screenHeight * 0.7, child: SingleChildScrollView( child: Padding( diff --git a/lib/services/batch_control_devices_service.dart b/lib/services/batch_control_devices_service.dart index f78cdef4..16542c8c 100644 --- a/lib/services/batch_control_devices_service.dart +++ b/lib/services/batch_control_devices_service.dart @@ -11,7 +11,8 @@ abstract interface class BatchControlDevicesService { }); } -final class RemoteBatchControlDevicesService implements BatchControlDevicesService { +final class RemoteBatchControlDevicesService + implements BatchControlDevicesService { @override Future batchControlDevices({ required List uuids, From ca41aa622479d28c10478620546e8447bfcbac9f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 16:37:09 +0300 Subject: [PATCH 090/105] all dates in heatmap are utc. --- .../analytics/models/occupancy_heat_map_model.dart | 14 +++++++++----- .../occupancy/widgets/interactive_heat_map.dart | 2 +- .../occupancy/widgets/occupancy_heat_map.dart | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 60d1a453..e4b7d730 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,14 +14,18 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map json) { - final eventDate = json['event_date'] as String? ?? '${DateTime.now()}'; - final year = eventDate.split('-')[0]; - final month = eventDate.split('-')[1]; - final day = eventDate.split('-')[2]; + final eventDate = json['event_date'] as String?; + final year = eventDate?.split('-')[0]; + final month = eventDate?.split('-')[1]; + final day = eventDate?.split('-')[2]; return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime(int.parse(year), int.parse(month), int.parse(day)), + eventDate: DateTime( + int.parse(year ?? '2025'), + int.parse(month ?? '1'), + int.parse(day ?? '1'), + ).toUtc(), countTotalPresenceDetected: num.parse( json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), diff --git a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart index a652ae73..514ebb65 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart @@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State { color: Colors.transparent, child: Transform.translate( offset: Offset(-(widget.cellSize * 2.5), -50), - child: HeatMapTooltip(date: item.date, value: item.value), + child: HeatMapTooltip(date: item.date.toUtc(), value: item.value), ), ), ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 05415421..0809a990 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc(); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } List _generatePaintItems(DateTime startDate) { return List.generate(_totalWeeks * 7, (index) { - final date = startDate.add(Duration(days: index)); + final date = startDate.toUtc().add(Duration(days: index)); final value = heatMapData[date] ?? 0; return OccupancyPaintItem(index: index, value: value, date: date); }); From 812c51400b984a5fe553f0bb57fe987a9e4b419c Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 16:53:40 +0300 Subject: [PATCH 091/105] add listener to batch --- .../bloc/curtain_module_bloc.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart index c3cd0b92..b40d7ea6 100644 --- a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -253,6 +253,37 @@ class CurtainModuleBloc extends Bloc { emit(CurtainModuleStatusLoaded( curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), )); + + Map statusMap = {}; + final ref = FirebaseDatabase.instance + .ref('device-status/${event.devicesIds.first}'); + final stream = ref.onValue; + + stream.listen((DatabaseEvent DatabaseEvent) async { + if (DatabaseEvent.snapshot.value == null) return; + + Map usersMap = + DatabaseEvent.snapshot.value as Map; + + List statusList = []; + + usersMap['status'].forEach((element) { + statusList + .add(Status(code: element['code'], value: element['value'])); + }); + + statusMap = { + for (final element in statusList) element.code: element.value, + }; + if (!isClosed) { + add( + ChangeCurtainModuleStatusEvent( + deviceId: event.devicesIds.first, + status: CurtainModuleStatusModel.fromJson(statusMap), + ), + ); + } + }); } catch (e) { emit(CurtainModuleError(message: e.toString())); } From 4744009cb65c887a80346a898733c032813877d7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 17:11:17 +0300 Subject: [PATCH 092/105] Update TotalEnergyConsumptionChart to adjust Y-axis limits and intervals for better data representation --- .../widgets/total_energy_consumption_chart.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 85b95c29..bba1fa14 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget { return Expanded( child: LineChart( LineChartData( + maxY: chartData.isEmpty + ? null + : chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250, clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, - leftTitlesInterval: 250, + leftTitlesInterval: 500, ), gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, - horizontalInterval: 250, + horizontalInterval: 500, ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(), @@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { ), duration: Duration.zero, curve: Curves.easeIn, - ), ); } From 0c0bf96c073dfd090cfb93c8a1a48c477cd3dbf8 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:16:41 +0300 Subject: [PATCH 093/105] add bloc builder to use the context --- .../view/curtain_module_items.dart | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 198c8713..07dc4158 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; import 'package:syncrow_web/services/batch_control_devices_service.dart'; import 'package:syncrow_web/services/control_device_service.dart'; @@ -24,7 +26,11 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { controlDeviceService: RemoteControlDeviceService(), batchControlDevicesService: RemoteBatchControlDevicesService()) ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), - child: _buildStatusControls(context), + child: BlocBuilder( + builder: (context, state) { + return _buildStatusControls(context); + }, + ), ); } @@ -41,19 +47,28 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { height: 10, ), SizedBox( - height: 120, + height: 140, width: 350, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: IconNameStatusContainer( - isFullIcon: false, - name: 'Schedules', - icon: Assets.schedule, - onTap: () {}, - status: false, - textColor: ColorsManager.blackColor, + child: ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'CUR_2', + ), + )); + }, + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ), const SizedBox( From 26e8ff7ee2adfe95fbcb70105245b4011ad4e217 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:55:27 +0300 Subject: [PATCH 094/105] use dynamic instead of bool to accept mant types and fix schedual view to accept curtain code and value --- .../schedule_widgets/schedual_view.dart | 16 +++++++++++++--- .../helper/add_schedule_dialog_helper.dart | 6 +++++- lib/services/devices_mang_api.dart | 3 +-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 47534d37..3b7bc138 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -12,10 +12,17 @@ import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_sched import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { - const BuildScheduleView( - {super.key, required this.deviceUuid, required this.category}); + const BuildScheduleView({ + super.key, + required this.deviceUuid, + required this.category, + this.code, + this.value, + }); final String deviceUuid; final String category; + final String? code; + final String? value; @override Widget build(BuildContext context) { @@ -59,11 +66,14 @@ class BuildScheduleView extends StatelessWidget { context, schedule: null, isEdit: false, + code: code, + value: value, ); if (entry != null) { context.read().add( ScheduleAddEvent( category: entry.category, + code: entry.function.code, time: entry.time, functionOn: entry.function.value, selectedDays: entry.days, @@ -74,7 +84,7 @@ class BuildScheduleView extends StatelessWidget { ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - CountdownInchingView( + CountdownInchingView( deviceId: deviceUuid, ), const SizedBox(height: 20), diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index ae7feac9..b8059402 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -17,6 +17,8 @@ class ScheduleDialogHelper { BuildContext context, { ScheduleEntry? schedule, bool isEdit = false, + String? code, + String? value, }) { final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) @@ -118,7 +120,9 @@ class ScheduleDialogHelper { final entry = ScheduleEntry( category: schedule?.category ?? 'switch_1', time: _formatTimeOfDayToISO(selectedTime), - function: Status(code: 'switch_1', value: functionOn), + function: Status( + code: code ?? 'switch_1', + value: value ?? functionOn), days: _convertSelectedDaysToStrings(selectedDays), scheduleId: schedule?.scheduleId, ); diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6fb27daf..963738a7 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -393,7 +393,7 @@ class DevicesManagementApi { required String deviceId, required String time, required String code, - required bool value, + required dynamic value, required List days, }) async { final response = await HTTPService().post( @@ -416,5 +416,4 @@ class DevicesManagementApi { ); return response; } - } From e365aa3faac702c29430f5cde7755f3bebdadc0b Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:56:12 +0300 Subject: [PATCH 095/105] edite event and block of schdual to accept code and functionOn as dynamic --- .../schedule_device/bloc/schedule_bloc.dart | 2 +- .../schedule_device/bloc/schedule_event.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index fbf7ae64..0ec55e39 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -265,7 +265,7 @@ class ScheduleBloc extends Bloc { category: event.category, deviceId: deviceId, time: getTimeStampWithoutSeconds(dateTime).toString(), - code: event.category, + code: event.code ?? event.category, value: event.functionOn, days: event.selectedDays); if (success) { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 0b9ec581..a28b8757 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -70,17 +70,19 @@ class ScheduleAddEvent extends ScheduleEvent { final String category; final String time; final List selectedDays; - final bool functionOn; + final dynamic functionOn; + final String? code; const ScheduleAddEvent({ required this.category, required this.time, required this.selectedDays, required this.functionOn, + required this.code, }); @override - List get props => [category, time, selectedDays, functionOn]; + List get props => [category, time, selectedDays, functionOn, code]; } class ScheduleEditEvent extends ScheduleEvent { From 1d95915f57be5788db5b458d2aa989ede7d67ac8 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:56:41 +0300 Subject: [PATCH 096/105] fix the string for motor without underscore --- .../curtain_module/widgets/prefrences_dialog.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart index bf500792..1e4f932c 100644 --- a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -69,7 +69,8 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { }, ), PrefReversCardWidget( - title: state.curtainModuleStatus.elecMachineryMode, + title: formatDeviceType( + state.curtainModuleStatus.elecMachineryMode), body: 'Motor Mode', onTap: () => context.read().add( ChangeElecMachineryModeEvent( @@ -136,4 +137,13 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { ), ); } + + String formatDeviceType(String raw) { + return raw + .split('_') + .map((word) => word.isNotEmpty + ? '${word[0].toUpperCase()}${word.substring(1)}' + : '') + .join(' '); + } } From 32208c1e81976a09d1311a072b7bc0dc39a5a94f Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Fri, 27 Jun 2025 17:57:04 +0300 Subject: [PATCH 097/105] send code and value to schdual from curtain module --- .../curtain_module/view/curtain_module_items.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 07dc4158..48b5ab2a 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -63,6 +63,8 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { child: BuildScheduleView( deviceUuid: deviceId, category: 'CUR_2', + code: 'control', + value: 'open', ), )); }, From 13e9a808ab70e0fe745d25ac9eadfb72cfcf5845 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 22:09:00 +0300 Subject: [PATCH 098/105] uses UTC dates as an attempt to fix heatmap's rendering bug. --- lib/pages/analytics/models/occupancy_heat_map_model.dart | 4 ++-- .../modules/occupancy/widgets/occupancy_heat_map.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index e4b7d730..4c7a37d4 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -21,11 +21,11 @@ class OccupancyHeatMapModel extends Equatable { return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime( + eventDate: DateTime.utc( int.parse(year ?? '2025'), int.parse(month ?? '1'), int.parse(day ?? '1'), - ).toUtc(), + ), countTotalPresenceDetected: num.parse( json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 0809a990..05c5bb71 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc(); + final jan1 = DateTime.utc(DateTime.now().year, 1, 1); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } List _generatePaintItems(DateTime startDate) { return List.generate(_totalWeeks * 7, (index) { - final date = startDate.toUtc().add(Duration(days: index)); + final date = startDate.add(Duration(days: index)); final value = heatMapData[date] ?? 0; return OccupancyPaintItem(index: index, value: value, date: date); }); From c4fd90b3bca1aa3ca390b1330c606c3e3c32fee1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sat, 28 Jun 2025 13:16:06 +0300 Subject: [PATCH 099/105] testing heatmap fixes. --- .../modules/occupancy/widgets/occupancy_heat_map.dart | 9 +++++++-- .../occupancy/widgets/occupancy_heat_map_box.dart | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 05415421..482f0029 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_ import 'package:syncrow_web/utils/color_manager.dart'; class OccupancyHeatMap extends StatelessWidget { - const OccupancyHeatMap({required this.heatMapData, super.key}); + const OccupancyHeatMap({ + required this.heatMapData, + required this.selectedDate, + super.key, + }); final Map heatMapData; + final DateTime selectedDate; static const _cellSize = 16.0; static const _totalWeeks = 53; @@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime.utc(selectedDate.year, 1, 1); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart index c3b537e0..a5f56aa4 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -70,6 +70,8 @@ class OccupancyHeatMapBox extends StatelessWidget { const SizedBox(height: 20), Expanded( child: OccupancyHeatMap( + selectedDate: + context.watch().state.yearlyDate, heatMapData: state.heatMapData.asMap().map( (_, value) => MapEntry( value.eventDate, From b97183fb61baef13f4018c6202ede44814f0f8ce Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sat, 28 Jun 2025 15:47:42 +0400 Subject: [PATCH 100/105] SP-1801 --- .../device_managment/ac/bloc/ac_bloc.dart | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 38d11a46..2ac26b41 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -45,8 +45,7 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -74,22 +73,18 @@ class AcBloc extends Bloc { void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - _deviceStatusSubscription = - ref.onValue.listen((DatabaseEvent event) async { + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; - Map usersMap = - event.snapshot.value as Map; + final usersMap = event.snapshot.value! as Map; - List statusList = []; + final statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = - AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); print('Device status updated: ${deviceStatus.acSwitch}'); if (!isClosed) { @@ -111,21 +106,14 @@ class AcBloc extends Bloc { AcControlEvent event, Emitter emit, ) async { - emit(AcsLoadingState()); - try { - final success = await controlDeviceService.controlDevice( + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); + await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); - if (!success) { - emit(const AcsFailedState(error: 'Failed to control device')); - } - } catch (e) { - emit(AcsFailedState(error: e.toString())); - } + } catch (e) {} } FutureOr _onFetchAcBatchStatus( @@ -134,10 +122,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -148,23 +134,16 @@ class AcBloc extends Bloc { AcBatchControlEvent event, Emitter emit, ) async { - emit(AcsLoadingState()); _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); try { - final success = await batchControlDevicesService.batchControlDevices( + await batchControlDevicesService.batchControlDevices( uuids: event.devicesIds, code: event.code, value: event.value, ); - - if (!success) { - emit(const AcsFailedState(error: 'Failed to control devices')); - } - } catch (e) { - emit(AcsFailedState(error: e.toString())); - } + } catch (e) {} } Future _onFactoryReset( @@ -197,8 +176,8 @@ class AcBloc extends Bloc { void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; - int newHours = scheduledHours; - int newMinutes = scheduledMinutes + 30; + var newHours = scheduledHours; + var newMinutes = scheduledMinutes + 30; newHours += newMinutes ~/ 60; newMinutes = newMinutes % 60; if (newHours > 23) { @@ -220,7 +199,7 @@ class AcBloc extends Bloc { ) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; - int totalMinutes = (scheduledHours * 60) + scheduledMinutes; + var totalMinutes = (scheduledHours * 60) + scheduledMinutes; totalMinutes = (totalMinutes - 30).clamp(0, 1440); scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; @@ -293,7 +272,7 @@ class AcBloc extends Bloc { void _startCountdownTimer(Emitter emit) { _countdownTimer?.cancel(); - int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); + var totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (totalSeconds > 0) { From bd53388438f04f3a15b87562f0770d169cf91a81 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 09:17:22 +0300 Subject: [PATCH 101/105] make one API with new QP to filter on spacesId --- .../device_managment_bloc.dart | 14 ++-- .../bloc/routine_bloc/routine_bloc.dart | 80 +++++++++---------- lib/services/devices_mang_api.dart | 13 ++- lib/utils/constants/api_const.dart | 2 +- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 98b0c195..6fd23374 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -41,18 +41,17 @@ class DeviceManagementBloc _devices.clear(); var spaceBloc = event.context.read(); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (spaceBloc.state.selectedCommunities.isEmpty) { - devices = - await DevicesManagementApi().fetchDevices('', '', projectUuid); + devices = await DevicesManagementApi().fetchDevices(projectUuid); } else { for (var community in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; - for (var space in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(community, space, projectUuid)); - } + spacesList.forEach( + (element) => print(element), + ); + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } @@ -270,6 +269,7 @@ class DeviceManagementBloc return 'All'; } } + void _onSearchDevices( SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 3fd07834..f38ea994 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -170,45 +170,45 @@ class RoutineBloc extends Bloc { } } -Future _onLoadScenes( - LoadScenes event, Emitter emit) async { - emit(state.copyWith(isLoading: true, errorMessage: null)); - List scenes = []; - try { - BuildContext context = NavigationService.navigatorKey.currentContext!; - var createRoutineBloc = context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (createRoutineBloc.selectedSpaceId == '' && - createRoutineBloc.selectedCommunityId == '') { - var spaceBloc = context.read(); - for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = - spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - scenes.addAll( - await SceneApi.getScenes(spaceId, communityId, projectUuid)); + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + List scenes = []; + try { + BuildContext context = NavigationService.navigatorKey.currentContext!; + var createRoutineBloc = context.read(); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { + var spaceBloc = context.read(); + for (var communityId in spaceBloc.state.selectedCommunities) { + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + for (var spaceId in spacesList) { + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); + } } + } else { + scenes.addAll(await SceneApi.getScenes( + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } - } else { - scenes.addAll(await SceneApi.getScenes( - createRoutineBloc.selectedSpaceId, - createRoutineBloc.selectedCommunityId, - projectUuid)); - } - emit(state.copyWith( - scenes: scenes, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( + emit(state.copyWith( + scenes: scenes, isLoading: false, - loadScenesErrorMessage: 'Failed to load scenes', - errorMessage: '', - loadAutomationErrorMessage: '', - scenes: scenes)); + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', + scenes: scenes)); + } } -} Future _onLoadAutomation( LoadAutomation event, Emitter emit) async { @@ -936,16 +936,12 @@ Future _onLoadScenes( for (var communityId in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(communityId, spaceId, projectUuid)); - } + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } else { - devices.addAll(await DevicesManagementApi().fetchDevices( - createRoutineBloc.selectedCommunityId, - createRoutineBloc.selectedSpaceId, - projectUuid)); + devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid, + spacesId: [createRoutineBloc.selectedSpaceId])); } emit(state.copyWith(isLoading: false, devices: devices)); diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6fb27daf..a6b4a278 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -13,15 +13,13 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class DevicesManagementApi { Future> fetchDevices( - String communityId, String spaceId, String projectId) async { + String projectId, { + List? spacesId, + }) async { try { final response = await HTTPService().get( - path: communityId.isNotEmpty && spaceId.isNotEmpty - ? ApiEndpoints.getSpaceDevices - .replaceAll('{spaceUuid}', spaceId) - .replaceAll('{communityUuid}', communityId) - .replaceAll('{projectId}', projectId) - : ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), + queryParameters: {if (spacesId != null) 'spaces': spacesId}, + path: ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), showServerMessage: true, expectedResponseModel: (json) { List jsonData = json['data']; @@ -416,5 +414,4 @@ class DevicesManagementApi { ); return response; } - } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index eb7b6a3e..6dda1108 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -18,7 +18,7 @@ abstract class ApiEndpoints { static const String getAllDevices = '/projects/{projectId}/devices'; static const String getSpaceDevices = - '/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices'; + '/projects/{projectId}/devices'; static const String getDeviceStatus = '/devices/{uuid}/functions/status'; static const String getBatchStatus = '/devices/batch'; From 1828ffb87a2903c8d2340cb4758fef81e0bb23a8 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 09:17:57 +0300 Subject: [PATCH 102/105] remove print statment --- .../bloc/device_mgmt_bloc/device_managment_bloc.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 6fd23374..4063692e 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -47,9 +47,6 @@ class DeviceManagementBloc for (var community in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; - spacesList.forEach( - (element) => print(element), - ); devices.addAll(await DevicesManagementApi() .fetchDevices(projectUuid, spacesId: spacesList)); } From b96f65d2c2b0cc577996e94101eb5a57212063c4 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 10:42:18 +0300 Subject: [PATCH 103/105] fix the open close states when curtain module --- .../curtain_module/view/curtain_module_items.dart | 2 +- .../schedule_widgets/schedual_view.dart | 15 ++++++++++----- .../schedule_widgets/schedule_table.dart | 14 ++++++++++++-- .../helper/add_schedule_dialog_helper.dart | 14 +++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 48b5ab2a..82c812ce 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -64,7 +64,7 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { deviceUuid: deviceId, category: 'CUR_2', code: 'control', - value: 'open', + ), )); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 3b7bc138..c511b8bd 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart'; @@ -9,6 +10,7 @@ import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widg import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { @@ -17,12 +19,10 @@ class BuildScheduleView extends StatelessWidget { required this.deviceUuid, required this.category, this.code, - this.value, }); final String deviceUuid; final String category; final String? code; - final String? value; @override Widget build(BuildContext context) { @@ -64,15 +64,20 @@ class BuildScheduleView extends StatelessWidget { final entry = await ScheduleDialogHelper .showAddScheduleDialog( context, - schedule: null, + schedule: ScheduleEntry( + category: category, + time: '', + function: Status( + code: code.toString(), value: null), + days: [], + ), isEdit: false, code: code, - value: value, ); if (entry != null) { context.read().add( ScheduleAddEvent( - category: entry.category, + category: category, code: entry.function.code, time: entry.time, functionOn: entry.function.value, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 98ae0515..b23e48df 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -162,11 +162,18 @@ class _ScheduleTableView extends StatelessWidget { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + bool temp; + if (schedule.category == 'CUR_2') { + temp = schedule.function.value == 'open' ? true : false; + } else { + temp = schedule.function.value as bool; + } context.read().add( ScheduleUpdateEntryEvent( category: schedule.category, scheduleId: schedule.scheduleId, - functionOn: schedule.function.value, + functionOn: temp, + // schedule.function.value, enable: !schedule.enable, ), ); @@ -188,7 +195,10 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - Center(child: Text(schedule.function.value ? 'On' : 'Off')), + schedule.category == 'CUR_2' + ? Center( + child: Text(schedule.function.value == true ? 'open' : 'close')) + : Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index b8059402..51087704 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -18,7 +18,6 @@ class ScheduleDialogHelper { ScheduleEntry? schedule, bool isEdit = false, String? code, - String? value, }) { final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) @@ -117,12 +116,21 @@ class ScheduleDialogHelper { width: 100, child: ElevatedButton( onPressed: () { + dynamic temp; + if (schedule?.category == 'CUR_2') { + temp = functionOn! ? 'open' : 'close'; + } else { + temp = functionOn; + } + print(temp); final entry = ScheduleEntry( category: schedule?.category ?? 'switch_1', time: _formatTimeOfDayToISO(selectedTime), function: Status( - code: code ?? 'switch_1', - value: value ?? functionOn), + code: code ?? 'switch_1', + value: temp, + // functionOn, + ), days: _convertSelectedDaysToStrings(selectedDays), scheduleId: schedule?.scheduleId, ); From ec1bb5b609bc54f6fab908b212aa8c297ffedda4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 29 Jun 2025 10:49:00 +0300 Subject: [PATCH 104/105] added curtain icons. --- lib/pages/visitor_password/model/device_model.dart | 4 ++++ lib/utils/enum/device_types.dart | 2 ++ 2 files changed, 6 insertions(+) diff --git a/lib/pages/visitor_password/model/device_model.dart b/lib/pages/visitor_password/model/device_model.dart index f9711eed..75d00350 100644 --- a/lib/pages/visitor_password/model/device_model.dart +++ b/lib/pages/visitor_password/model/device_model.dart @@ -80,6 +80,10 @@ class DeviceModel { tempIcon = Assets.openedDoor; } else if (type == DeviceType.WaterLeak) { tempIcon = Assets.waterLeakNormal; + } else if (type == DeviceType.Curtain2) { + tempIcon = Assets.curtainIcon; + } else if (type == DeviceType.Curtain) { + tempIcon = Assets.curtainIcon; } else { tempIcon = Assets.blackLogo; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 9bfd322f..947e63aa 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -3,6 +3,7 @@ enum DeviceType { LightBulb, DoorLock, Curtain, + Curtain2, Blind, OneGang, TwoGang, @@ -44,6 +45,7 @@ enum DeviceType { Map devicesTypesMap = { "AC": DeviceType.AC, + "CUR_2": DeviceType.Curtain2, "GW": DeviceType.Gateway, "CPS": DeviceType.CeilingSensor, "DL": DeviceType.DoorLock, From a1562110d57e47f5d583c507b0f37b3abf3b6d97 Mon Sep 17 00:00:00 2001 From: Rafeek-Khoudare Date: Sun, 29 Jun 2025 10:50:51 +0300 Subject: [PATCH 105/105] add close open if it is curtain module for schdule --- .../water_heater/helper/add_schedule_dialog_helper.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 51087704..389eac3f 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -97,7 +97,8 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(ctx, functionOn!, (v) { + _buildFunctionSwitch(schedule!.category, ctx, functionOn!, + (v) { setState(() => functionOn = v); }), ], @@ -197,7 +198,7 @@ class ScheduleDialogHelper { } static Widget _buildFunctionSwitch( - BuildContext ctx, bool isOn, Function(bool) onChanged) { + String categor, BuildContext ctx, bool isOn, Function(bool) onChanged) { return Row( children: [ Text( @@ -211,14 +212,14 @@ class ScheduleDialogHelper { groupValue: isOn, onChanged: (val) => onChanged(true), ), - const Text('On'), + Text(categor == 'CUR_2' ? 'open' : 'On'), const SizedBox(width: 10), Radio( value: false, groupValue: isOn, onChanged: (val) => onChanged(false), ), - const Text('Off'), + Text(categor == 'CUR_2' ? 'close' : 'Off'), ], ); }